mirror of
https://github.com/meshtastic/Meshtastic-Apple.git
synced 2026-04-20 22:13:56 +00:00
Merge pull request #288 from meshtastic/2.0.11_Working_Changes
Merging this before it gets any worse
This commit is contained in:
commit
d6cc53397c
50 changed files with 3475 additions and 1779 deletions
|
|
@ -7,7 +7,6 @@
|
|||
objects = {
|
||||
|
||||
/* Begin PBXBuildFile section */
|
||||
C9483F6D2773017500998F6B /* MapView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C9483F6C2773017500998F6B /* MapView.swift */; };
|
||||
C9697F9D279336B700250207 /* LocalMBTileOverlay.swift in Sources */ = {isa = PBXBuildFile; fileRef = C9697F9C279336B700250207 /* LocalMBTileOverlay.swift */; };
|
||||
C9697FA527933B8C00250207 /* SQLite in Frameworks */ = {isa = PBXBuildFile; productRef = C9697FA427933B8C00250207 /* SQLite */; };
|
||||
C9A7BC1027759A9600760B50 /* PositionAnnotationView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C9A7BC0F27759A9600760B50 /* PositionAnnotationView.swift */; };
|
||||
|
|
@ -22,7 +21,10 @@
|
|||
DD23A50F26FD1B4400D9B90C /* PeripheralModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD23A50E26FD1B4400D9B90C /* PeripheralModel.swift */; };
|
||||
DD2553572855B02500E55709 /* LoRaConfig.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD2553562855B02500E55709 /* LoRaConfig.swift */; };
|
||||
DD2553592855B52700E55709 /* PositionConfig.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD2553582855B52700E55709 /* PositionConfig.swift */; };
|
||||
DD2AD8A8296D2DF9001FF0E7 /* MapViewSwiftUI.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD2AD8A7296D2DF9001FF0E7 /* MapViewSwiftUI.swift */; };
|
||||
DD2E65262767A01F00E45FC5 /* NodeDetail.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD2E65252767A01F00E45FC5 /* NodeDetail.swift */; };
|
||||
DD2F145129787595009E4638 /* xmodem.pb.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD2F144F29787595009E4638 /* xmodem.pb.swift */; };
|
||||
DD2F145229787595009E4638 /* rtttl.pb.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD2F145029787595009E4638 /* rtttl.pb.swift */; };
|
||||
DD3501892852FC3B000FC853 /* Settings.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD3501882852FC3B000FC853 /* Settings.swift */; };
|
||||
DD35018B2852FC79000FC853 /* UserSettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD35018A2852FC79000FC853 /* UserSettings.swift */; };
|
||||
DD3CC6B528E33FD100FA9159 /* ShareChannels.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD3CC6B428E33FD100FA9159 /* ShareChannels.swift */; };
|
||||
|
|
@ -66,6 +68,11 @@
|
|||
DD8ED9C8289CE4B900B3B0AB /* RoutingError.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD8ED9C7289CE4B900B3B0AB /* RoutingError.swift */; };
|
||||
DD90860E26F69BAE00DC5189 /* NodeMap.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD90860D26F69BAE00DC5189 /* NodeMap.swift */; };
|
||||
DD913639270DFF4C00D7ACF3 /* LocalNotificationManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD913638270DFF4C00D7ACF3 /* LocalNotificationManager.swift */; };
|
||||
DD964FBD296E6B01007C176F /* EmojiOnlyTextField.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD964FBC296E6B01007C176F /* EmojiOnlyTextField.swift */; };
|
||||
DD964FBF296E76EF007C176F /* WaypointFormView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD964FBE296E76EF007C176F /* WaypointFormView.swift */; };
|
||||
DD964FC2297272AE007C176F /* WaypointEntityExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD964FC1297272AE007C176F /* WaypointEntityExtension.swift */; };
|
||||
DD964FC42974767D007C176F /* MapViewFitExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD964FC32974767D007C176F /* MapViewFitExtension.swift */; };
|
||||
DD964FC62975DBFD007C176F /* QueryCoreData.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD964FC52975DBFD007C176F /* QueryCoreData.swift */; };
|
||||
DD97E96628EFD9820056DDA4 /* MeshtasticLogo.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD97E96528EFD9820056DDA4 /* MeshtasticLogo.swift */; };
|
||||
DD97E96828EFE9A00056DDA4 /* About.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD97E96728EFE9A00056DDA4 /* About.swift */; };
|
||||
DD994B69295F88B60013760A /* IntervalEnums.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD994B68295F88B60013760A /* IntervalEnums.swift */; };
|
||||
|
|
@ -127,7 +134,7 @@
|
|||
/* End PBXContainerItemProxy section */
|
||||
|
||||
/* Begin PBXFileReference section */
|
||||
C9483F6C2773017500998F6B /* MapView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MapView.swift; sourceTree = "<group>"; };
|
||||
A65FA974296876BF00A97686 /* zh-Hans */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "zh-Hans"; path = "zh-Hans.lproj/Localizable.strings"; sourceTree = "<group>"; };
|
||||
C9697F9C279336B700250207 /* LocalMBTileOverlay.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocalMBTileOverlay.swift; sourceTree = "<group>"; };
|
||||
C9A7BC0F27759A9600760B50 /* PositionAnnotationView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PositionAnnotationView.swift; sourceTree = "<group>"; };
|
||||
C9A88B54278B503C00BD810A /* MapViewModule.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MapViewModule.swift; sourceTree = "<group>"; };
|
||||
|
|
@ -141,7 +148,10 @@
|
|||
DD23A50E26FD1B4400D9B90C /* PeripheralModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PeripheralModel.swift; sourceTree = "<group>"; };
|
||||
DD2553562855B02500E55709 /* LoRaConfig.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoRaConfig.swift; sourceTree = "<group>"; };
|
||||
DD2553582855B52700E55709 /* PositionConfig.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PositionConfig.swift; sourceTree = "<group>"; };
|
||||
DD2AD8A7296D2DF9001FF0E7 /* MapViewSwiftUI.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MapViewSwiftUI.swift; sourceTree = "<group>"; };
|
||||
DD2E65252767A01F00E45FC5 /* NodeDetail.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NodeDetail.swift; sourceTree = "<group>"; };
|
||||
DD2F144F29787595009E4638 /* xmodem.pb.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = xmodem.pb.swift; sourceTree = "<group>"; };
|
||||
DD2F145029787595009E4638 /* rtttl.pb.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = rtttl.pb.swift; sourceTree = "<group>"; };
|
||||
DD3501882852FC3B000FC853 /* Settings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Settings.swift; sourceTree = "<group>"; };
|
||||
DD35018A2852FC79000FC853 /* UserSettings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserSettings.swift; sourceTree = "<group>"; };
|
||||
DD3CC6B428E33FD100FA9159 /* ShareChannels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShareChannels.swift; sourceTree = "<group>"; };
|
||||
|
|
@ -187,6 +197,12 @@
|
|||
DD90860A26F645B700DC5189 /* Meshtastic.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = Meshtastic.entitlements; sourceTree = "<group>"; };
|
||||
DD90860D26F69BAE00DC5189 /* NodeMap.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NodeMap.swift; sourceTree = "<group>"; };
|
||||
DD913638270DFF4C00D7ACF3 /* LocalNotificationManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocalNotificationManager.swift; sourceTree = "<group>"; };
|
||||
DD964FBC296E6B01007C176F /* EmojiOnlyTextField.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EmojiOnlyTextField.swift; sourceTree = "<group>"; };
|
||||
DD964FBE296E76EF007C176F /* WaypointFormView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WaypointFormView.swift; sourceTree = "<group>"; };
|
||||
DD964FC029724F6D007C176F /* MeshtasticDataModelV6.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = MeshtasticDataModelV6.xcdatamodel; sourceTree = "<group>"; };
|
||||
DD964FC1297272AE007C176F /* WaypointEntityExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WaypointEntityExtension.swift; sourceTree = "<group>"; };
|
||||
DD964FC32974767D007C176F /* MapViewFitExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MapViewFitExtension.swift; sourceTree = "<group>"; };
|
||||
DD964FC52975DBFD007C176F /* QueryCoreData.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = QueryCoreData.swift; sourceTree = "<group>"; };
|
||||
DD97E96528EFD9820056DDA4 /* MeshtasticLogo.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MeshtasticLogo.swift; sourceTree = "<group>"; };
|
||||
DD97E96728EFE9A00056DDA4 /* About.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = About.swift; sourceTree = "<group>"; };
|
||||
DD994B68295F88B60013760A /* IntervalEnums.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IntervalEnums.swift; sourceTree = "<group>"; };
|
||||
|
|
@ -270,9 +286,7 @@
|
|||
isa = PBXGroup;
|
||||
children = (
|
||||
C9A7BC0E27759A6800760B50 /* Custom */,
|
||||
C9483F6C2773017500998F6B /* MapView.swift */,
|
||||
C9A88B54278B503C00BD810A /* MapViewModule.swift */,
|
||||
C9697F9C279336B700250207 /* LocalMBTileOverlay.swift */,
|
||||
DD964FBE296E76EF007C176F /* WaypointFormView.swift */,
|
||||
);
|
||||
path = Map;
|
||||
sourceTree = "<group>";
|
||||
|
|
@ -280,6 +294,10 @@
|
|||
C9A7BC0E27759A6800760B50 /* Custom */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
C9697F9C279336B700250207 /* LocalMBTileOverlay.swift */,
|
||||
DD964FC32974767D007C176F /* MapViewFitExtension.swift */,
|
||||
DD2AD8A7296D2DF9001FF0E7 /* MapViewSwiftUI.swift */,
|
||||
C9A88B54278B503C00BD810A /* MapViewModule.swift */,
|
||||
C9A7BC0F27759A9600760B50 /* PositionAnnotationView.swift */,
|
||||
);
|
||||
path = Custom;
|
||||
|
|
@ -391,21 +409,23 @@
|
|||
DDAF8C5626ED07740058C060 /* Protobufs */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
DDB3107128A6224100F1DE3D /* device_metadata.pb.swift */,
|
||||
DDCFF600285453A7005FA625 /* localonly.pb.swift */,
|
||||
DD4DED8F27AD2975004BA27E /* cannedmessages.pb.swift */,
|
||||
DDAF8C6126ED0A230058C060 /* admin.pb.swift */,
|
||||
C9A88B56278B559900BD810A /* apponly.pb.swift */,
|
||||
DD4DED8F27AD2975004BA27E /* cannedmessages.pb.swift */,
|
||||
DDAF8C6426ED0A490058C060 /* channel.pb.swift */,
|
||||
DD4C158D2824AA7E0032668E /* config.pb.swift */,
|
||||
DDAF8C6826ED0D070058C060 /* deviceonly.pb.swift */,
|
||||
DDB3107128A6224100F1DE3D /* device_metadata.pb.swift */,
|
||||
DDCFF600285453A7005FA625 /* localonly.pb.swift */,
|
||||
DDAF8C5726ED07FD0058C060 /* mesh.pb.swift */,
|
||||
DD4C158B2824A91E0032668E /* module_config.pb.swift */,
|
||||
DDAF8C6026ED0A230058C060 /* mqtt.pb.swift */,
|
||||
DDAF8C5C26ED09490058C060 /* portnums.pb.swift */,
|
||||
DDAF8C6626ED0C8C0058C060 /* remote_hardware.pb.swift */,
|
||||
DD2F145029787595009E4638 /* rtttl.pb.swift */,
|
||||
DD17E5DC277D49D400010EC2 /* storeforward.pb.swift */,
|
||||
DDB2CC6D27F3EB47009C5FCC /* telemetry.pb.swift */,
|
||||
DD2F144F29787595009E4638 /* xmodem.pb.swift */,
|
||||
);
|
||||
path = Protobufs;
|
||||
sourceTree = "<group>";
|
||||
|
|
@ -546,6 +566,7 @@
|
|||
DD913638270DFF4C00D7ACF3 /* LocalNotificationManager.swift */,
|
||||
DD8169F8271F1A6100F4AB02 /* MeshLogger.swift */,
|
||||
DDA6B2E828419CF2003E8C16 /* MeshPackets.swift */,
|
||||
DD964FBC296E6B01007C176F /* EmojiOnlyTextField.swift */,
|
||||
);
|
||||
path = Helpers;
|
||||
sourceTree = "<group>";
|
||||
|
|
@ -557,7 +578,9 @@
|
|||
DD5394FD276BA0EF00AD86B1 /* PositionEntityExtension.swift */,
|
||||
DDD9E4E3284B208E003777C5 /* UserEntityExtension.swift */,
|
||||
DD58C5F12919AD3C00D5BEFB /* ChannelEntityExtension.swift */,
|
||||
DD964FC52975DBFD007C176F /* QueryCoreData.swift */,
|
||||
DD3CC6C128EB9D4900FA9159 /* UpdateCoreData.swift */,
|
||||
DD964FC1297272AE007C176F /* WaypointEntityExtension.swift */,
|
||||
);
|
||||
path = Persistence;
|
||||
sourceTree = "<group>";
|
||||
|
|
@ -655,6 +678,7 @@
|
|||
en,
|
||||
de,
|
||||
Base,
|
||||
"zh-Hans",
|
||||
);
|
||||
mainGroup = DDC2E14B26CE248E0042C5E4;
|
||||
packageReferences = (
|
||||
|
|
@ -729,6 +753,7 @@
|
|||
DDCFF601285453A7005FA625 /* localonly.pb.swift in Sources */,
|
||||
DD836AE726F6B38600ABCC23 /* Connect.swift in Sources */,
|
||||
DDAF8C6E26ED19040058C060 /* Extensions.swift in Sources */,
|
||||
DD964FBF296E76EF007C176F /* WaypointFormView.swift in Sources */,
|
||||
DD3501892852FC3B000FC853 /* Settings.swift in Sources */,
|
||||
DD5D0A9C2931B9F200F7EA61 /* EthernetModes.swift in Sources */,
|
||||
DD798B072915928D005217CD /* ChannelMessageList.swift in Sources */,
|
||||
|
|
@ -736,6 +761,7 @@
|
|||
DDC2E1A726CEB3400042C5E4 /* LocationHelper.swift in Sources */,
|
||||
DD5394FE276BA0EF00AD86B1 /* PositionEntityExtension.swift in Sources */,
|
||||
DD913639270DFF4C00D7ACF3 /* LocalNotificationManager.swift in Sources */,
|
||||
DD2F145129787595009E4638 /* xmodem.pb.swift in Sources */,
|
||||
DD4C158C2824A91E0032668E /* module_config.pb.swift in Sources */,
|
||||
DD4F23CD28779A3C001D37CB /* EnvironmentMetricsLog.swift in Sources */,
|
||||
DDB6ABD628AE742000384BA1 /* BluetoothConfig.swift in Sources */,
|
||||
|
|
@ -743,7 +769,9 @@
|
|||
DDAF8C5326EB1DF10058C060 /* BLEManager.swift in Sources */,
|
||||
DDC4D568275499A500A4208E /* Persistence.swift in Sources */,
|
||||
DD90860E26F69BAE00DC5189 /* NodeMap.swift in Sources */,
|
||||
DD964FBD296E6B01007C176F /* EmojiOnlyTextField.swift in Sources */,
|
||||
DD8169FF272476C700F4AB02 /* LogDocument.swift in Sources */,
|
||||
DD2AD8A8296D2DF9001FF0E7 /* MapViewSwiftUI.swift in Sources */,
|
||||
DD6193792863875F00E59241 /* SerialConfig.swift in Sources */,
|
||||
DDA0B6B2294CDC55001356EC /* Channels.swift in Sources */,
|
||||
DDAF8C6926ED0D070058C060 /* deviceonly.pb.swift in Sources */,
|
||||
|
|
@ -766,8 +794,10 @@
|
|||
DDB6ABDB28B0AC6000384BA1 /* DistanceText.swift in Sources */,
|
||||
C9A7BC1027759A9600760B50 /* PositionAnnotationView.swift in Sources */,
|
||||
DD882F5D2772E4640005BF05 /* Contacts.swift in Sources */,
|
||||
DD964FC2297272AE007C176F /* WaypointEntityExtension.swift in Sources */,
|
||||
DD47E3CE26F103C600029299 /* NodeList.swift in Sources */,
|
||||
DD8EBF43285058FA00426DCA /* DisplayConfig.swift in Sources */,
|
||||
DD964FC42974767D007C176F /* MapViewFitExtension.swift in Sources */,
|
||||
DD47E3D626F17ED900029299 /* CircleText.swift in Sources */,
|
||||
DDC2E18F26CE25FE0042C5E4 /* ContentView.swift in Sources */,
|
||||
DD17E5DE277D49D400010EC2 /* storeforward.pb.swift in Sources */,
|
||||
|
|
@ -783,8 +813,8 @@
|
|||
DD86D40C287F401000BAEB7A /* SaveChannelQRCode.swift in Sources */,
|
||||
DDA1C48E28DB49D3009933EC /* ChannelRoles.swift in Sources */,
|
||||
DD8ED9C8289CE4B900B3B0AB /* RoutingError.swift in Sources */,
|
||||
C9483F6D2773017500998F6B /* MapView.swift in Sources */,
|
||||
DDAF8C5826ED07FD0058C060 /* mesh.pb.swift in Sources */,
|
||||
DD964FC62975DBFD007C176F /* QueryCoreData.swift in Sources */,
|
||||
DD8169FB271F1F3A00F4AB02 /* MeshLog.swift in Sources */,
|
||||
DD8ED9C52898D51F00B3B0AB /* NetworkConfig.swift in Sources */,
|
||||
DDC3B274283F411B00AC321C /* LastHeardText.swift in Sources */,
|
||||
|
|
@ -809,6 +839,7 @@
|
|||
C9697F9D279336B700250207 /* LocalMBTileOverlay.swift in Sources */,
|
||||
DD58C5F22919AD3C00D5BEFB /* ChannelEntityExtension.swift in Sources */,
|
||||
DD0F791B28713C8A00A6FDAD /* AdminMessageList.swift in Sources */,
|
||||
DD2F145229787595009E4638 /* rtttl.pb.swift in Sources */,
|
||||
DD3CC6BC28E366DF00FA9159 /* Meshtastic.xcdatamodeld in Sources */,
|
||||
DD8169F9271F1A6100F4AB02 /* MeshLogger.swift in Sources */,
|
||||
DD539502276DAA6A00AD86B1 /* MapLocation.swift in Sources */,
|
||||
|
|
@ -857,6 +888,7 @@
|
|||
children = (
|
||||
DDCDC6CC29481FCC004C1DDA /* en */,
|
||||
DDCDC6CE294821AD004C1DDA /* de */,
|
||||
A65FA974296876BF00A97686 /* zh-Hans */,
|
||||
);
|
||||
name = Localizable.strings;
|
||||
sourceTree = "<group>";
|
||||
|
|
@ -1006,7 +1038,7 @@
|
|||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
);
|
||||
MARKETING_VERSION = 2.0.10;
|
||||
MARKETING_VERSION = 2.0.11;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = gvh.MeshtasticClient;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SUPPORTS_MACCATALYST = YES;
|
||||
|
|
@ -1039,7 +1071,7 @@
|
|||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
);
|
||||
MARKETING_VERSION = 2.0.10;
|
||||
MARKETING_VERSION = 2.0.11;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = gvh.MeshtasticClient;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SUPPORTS_MACCATALYST = YES;
|
||||
|
|
@ -1215,13 +1247,14 @@
|
|||
DD3CC6BA28E366DF00FA9159 /* Meshtastic.xcdatamodeld */ = {
|
||||
isa = XCVersionGroup;
|
||||
children = (
|
||||
DD964FC029724F6D007C176F /* MeshtasticDataModelV6.xcdatamodel */,
|
||||
DD457BC4295D5E35004BCE4D /* MeshtasticDataModelV5.xcdatamodel */,
|
||||
DDEE03EC29544A1000FCAD57 /* MeshtasticDataModelV4.xcdatamodel */,
|
||||
DDCDC69A29467643004C1DDA /* MeshtasticDataModelV3.xcdatamodel */,
|
||||
DD5D0A9A2931AD6B00F7EA61 /* MeshtasticDataModelV2.xcdatamodel */,
|
||||
DD3CC6BB28E366DF00FA9159 /* MeshtasticDataModel.xcdatamodel */,
|
||||
);
|
||||
currentVersion = DD457BC4295D5E35004BCE4D /* MeshtasticDataModelV5.xcdatamodel */;
|
||||
currentVersion = DD964FC029724F6D007C176F /* MeshtasticDataModelV6.xcdatamodel */;
|
||||
name = Meshtastic.xcdatamodeld;
|
||||
path = Meshtastic/Meshtastic.xcdatamodeld;
|
||||
sourceTree = "<group>";
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@
|
|||
//
|
||||
|
||||
import Foundation
|
||||
import MapKit
|
||||
|
||||
enum KeyboardType: Int, CaseIterable, Identifiable {
|
||||
|
||||
|
|
@ -36,24 +37,51 @@ enum KeyboardType: Int, CaseIterable, Identifiable {
|
|||
|
||||
enum MeshMapType: String, CaseIterable, Identifiable {
|
||||
|
||||
case satellite = "satellite"
|
||||
case standard = "standard"
|
||||
case mutedStandard = "mutedStandard"
|
||||
case hybrid = "hybrid"
|
||||
case standard = "standard"
|
||||
case hybridFlyover = "hybridFlyover"
|
||||
case satellite = "satellite"
|
||||
case satelliteFlyover = "satelliteFlyover"
|
||||
|
||||
|
||||
var id: String { self.rawValue }
|
||||
|
||||
var description: String {
|
||||
get {
|
||||
switch self {
|
||||
case .satellite:
|
||||
return NSLocalizedString("satellite", comment: "Satellite Map Type")
|
||||
case .standard:
|
||||
return NSLocalizedString("standard", comment: "Standard Map Type")
|
||||
return NSLocalizedString("standard", comment: "Standard")
|
||||
case .mutedStandard:
|
||||
return NSLocalizedString("standard.muted", comment: "Standard Muted")
|
||||
case .hybrid:
|
||||
return NSLocalizedString("hybrid", comment: "Hybrid Map Type")
|
||||
return NSLocalizedString("hybrid", comment: "Hybrid")
|
||||
case .hybridFlyover:
|
||||
return NSLocalizedString("hybrid.flyover", comment: "Hybrid Flyover")
|
||||
case .satellite:
|
||||
return NSLocalizedString("satellite", comment: "Satellite")
|
||||
case .satelliteFlyover:
|
||||
return NSLocalizedString("satellite.flyover", comment: "Satellite Flyover")
|
||||
}
|
||||
}
|
||||
}
|
||||
func MKMapTypeValue() -> MKMapType {
|
||||
|
||||
switch self {
|
||||
case .standard:
|
||||
return MKMapType.standard
|
||||
case .mutedStandard:
|
||||
return MKMapType.mutedStandard
|
||||
case .hybrid:
|
||||
return MKMapType.hybrid
|
||||
case .hybridFlyover:
|
||||
return MKMapType.hybridFlyover
|
||||
case .satellite:
|
||||
return MKMapType.satellite
|
||||
case .satelliteFlyover:
|
||||
return MKMapType.satelliteFlyover
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
enum LocationUpdateInterval: Int, CaseIterable, Identifiable {
|
||||
|
|
|
|||
|
|
@ -104,6 +104,7 @@ enum OledTypes: Int, CaseIterable, Identifiable {
|
|||
case auto = 0
|
||||
case ssd1306 = 1
|
||||
case sh1106 = 2
|
||||
//case sh1107 = 3
|
||||
|
||||
var id: Int { self.rawValue }
|
||||
var description: String {
|
||||
|
|
@ -115,6 +116,8 @@ enum OledTypes: Int, CaseIterable, Identifiable {
|
|||
return "SSD 1306"
|
||||
case .sh1106:
|
||||
return "SH 1106"
|
||||
//case .sh1107:
|
||||
// return "SH 1107"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -127,6 +130,46 @@ enum OledTypes: Int, CaseIterable, Identifiable {
|
|||
return Config.DisplayConfig.OledType.oledSsd1306
|
||||
case .sh1106:
|
||||
return Config.DisplayConfig.OledType.oledSh1106
|
||||
//case .sh1107:
|
||||
// return Config.DisplayConfig.OledType.oledSh1107
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Default of 0 is auto
|
||||
enum DisplayModes: Int, CaseIterable, Identifiable {
|
||||
|
||||
case defaultMode = 0
|
||||
case twoColor = 1
|
||||
case inverted = 2
|
||||
case color = 3
|
||||
|
||||
var id: Int { self.rawValue }
|
||||
var description: String {
|
||||
get {
|
||||
switch self {
|
||||
case .defaultMode:
|
||||
return "Default 128x64 screen layout"
|
||||
case .twoColor:
|
||||
return "Optimized for 2 color displays"
|
||||
case .inverted:
|
||||
return "Inverted top bar for 2 Color display"
|
||||
case .color:
|
||||
return "TFT Full Color Displays"
|
||||
}
|
||||
}
|
||||
}
|
||||
func protoEnumValue() -> Config.DisplayConfig.DisplayMode {
|
||||
|
||||
switch self {
|
||||
case .defaultMode:
|
||||
return Config.DisplayConfig.DisplayMode.default
|
||||
case .twoColor:
|
||||
return Config.DisplayConfig.DisplayMode.twocolor
|
||||
case .inverted:
|
||||
return Config.DisplayConfig.DisplayMode.inverted
|
||||
case .color:
|
||||
return Config.DisplayConfig.DisplayMode.color
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ import SwiftUI
|
|||
func TelemetryToCsvFile(telemetry: [TelemetryEntity], metricsType: Int) -> String {
|
||||
var csvString: String = ""
|
||||
let localeDateFormat = DateFormatter.dateFormat(fromTemplate: "yyMMddjmma", options: 0, locale: Locale.current)
|
||||
let dateFormatString = (localeDateFormat ?? "MM/dd/YY j:mma")
|
||||
let dateFormatString = (localeDateFormat ?? "MM/dd/YY j:mma").replacingOccurrences(of: ",", with: "")
|
||||
if metricsType == 0 {
|
||||
// Create Device Metrics Header
|
||||
csvString = "\(NSLocalizedString("battery.level", comment: "")), \(NSLocalizedString("voltage", comment: "")), \(NSLocalizedString("channel.utilization", comment: "")), \(NSLocalizedString("airtime", comment: "")), \(NSLocalizedString("timestamp", comment: ""))"
|
||||
|
|
@ -56,7 +56,7 @@ func TelemetryToCsvFile(telemetry: [TelemetryEntity], metricsType: Int) -> Strin
|
|||
func PositionToCsvFile(positions: [PositionEntity]) -> String {
|
||||
var csvString: String = ""
|
||||
let localeDateFormat = DateFormatter.dateFormat(fromTemplate: "yyMMddjmma", options: 0, locale: Locale.current)
|
||||
let dateFormatString = (localeDateFormat ?? "MM/dd/YY j:mma")
|
||||
let dateFormatString = (localeDateFormat ?? "MM/dd/YY j:mma").replacingOccurrences(of: ",", with: "")
|
||||
// Create Position Header
|
||||
csvString = "SeqNo, Latitude, Longitude, Altitude, Sats, Speed, Heading, SNR, \(NSLocalizedString("timestamp", comment: ""))"
|
||||
for pos in positions {
|
||||
|
|
|
|||
|
|
@ -291,9 +291,9 @@ class BLEManager: NSObject, CBPeripheralDelegate, ObservableObject {
|
|||
}
|
||||
}
|
||||
|
||||
func requestDeviceMetadata() {
|
||||
func requestDeviceMetadata(fromUser: UserEntity, toUser: UserEntity, adminIndex: Int32, context: NSManagedObjectContext) -> Int64 {
|
||||
|
||||
guard (connectedPeripheral!.peripheral.state == CBPeripheralState.connected) else { return }
|
||||
guard (connectedPeripheral!.peripheral.state == CBPeripheralState.connected) else { return 0 }
|
||||
|
||||
let nodeName = connectedPeripheral!.peripheral.name ?? NSLocalizedString("unknown", comment: NSLocalizedString("unknown", comment: "Unknown"))
|
||||
let logString = String.localizedStringWithFormat(NSLocalizedString("mesh.log.devicemetadata %@",
|
||||
|
|
@ -303,19 +303,20 @@ class BLEManager: NSObject, CBPeripheralDelegate, ObservableObject {
|
|||
adminPacket.getDeviceMetadataRequest = true
|
||||
var meshPacket: MeshPacket = MeshPacket()
|
||||
meshPacket.id = UInt32.random(in: UInt32(UInt8.max)..<UInt32.max)
|
||||
meshPacket.to = UInt32(toUser.num)
|
||||
meshPacket.from = UInt32(fromUser.num)
|
||||
meshPacket.priority = MeshPacket.Priority.reliable
|
||||
meshPacket.wantAck = true
|
||||
meshPacket.channel = UInt32(adminIndex)
|
||||
var dataMessage = DataMessage()
|
||||
dataMessage.payload = try! adminPacket.serializedData()
|
||||
dataMessage.portnum = PortNum.adminApp
|
||||
dataMessage.wantResponse = true
|
||||
meshPacket.decoded = dataMessage
|
||||
var toRadio: ToRadio = ToRadio()
|
||||
toRadio.packet = meshPacket
|
||||
let binaryData: Data = try! toRadio.serializedData()
|
||||
connectedPeripheral!.peripheral.writeValue(binaryData, for: TORADIO_characteristic, type: .withResponse)
|
||||
// Either Read the config complete value or from num notify value
|
||||
connectedPeripheral!.peripheral.readValue(for: FROMRADIO_characteristic)
|
||||
let messageDescription = "🛎️ Requested Device Metadata for node \(toUser.longName ?? NSLocalizedString("unknown", comment: "Unknown")) by \(fromUser.longName ?? NSLocalizedString("unknown", comment: "Unknown"))"
|
||||
if sendAdminMessageToRadio(meshPacket: meshPacket, adminDescription: messageDescription, fromUser: fromUser, toUser: toUser) {
|
||||
return Int64(meshPacket.id)
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
func sendTraceRouteRequest(destNum: Int64, wantResponse: Bool) -> Bool {
|
||||
|
|
@ -504,9 +505,9 @@ class BLEManager: NSObject, CBPeripheralDelegate, ObservableObject {
|
|||
case .remoteHardwareApp:
|
||||
MeshLogger.log("🕸️ MESH PACKET received for Remote Hardware App UNHANDLED \(try! decodedInfo.packet.jsonString())")
|
||||
case .positionApp:
|
||||
positionPacket(packet: decodedInfo.packet, context: context!)
|
||||
upsertPositionPacket(packet: decodedInfo.packet, context: context!)
|
||||
case .waypointApp:
|
||||
MeshLogger.log("🕸️ MESH PACKET received for Waypoint App UNHANDLED \(try! decodedInfo.packet.jsonString())")
|
||||
waypointPacket(packet: decodedInfo.packet, context: context!)
|
||||
case .nodeinfoApp:
|
||||
if !invalidVersion { nodeInfoAppPacket(packet: decodedInfo.packet, context: context!) }
|
||||
case .routingApp:
|
||||
|
|
@ -627,7 +628,7 @@ class BLEManager: NSObject, CBPeripheralDelegate, ObservableObject {
|
|||
if preferredPeripheral != nil && preferredPeripheral?.peripheral != nil {
|
||||
connectTo(peripheral: preferredPeripheral!.peripheral)
|
||||
}
|
||||
let nodeName = connectedPeripheral!.peripheral.name ?? NSLocalizedString("unknown", comment: NSLocalizedString("unknown", comment: "Unknown"))
|
||||
let nodeName = connectedPeripheral?.peripheral.name ?? NSLocalizedString("unknown", comment: NSLocalizedString("unknown", comment: "Unknown"))
|
||||
let logString = String.localizedStringWithFormat(NSLocalizedString("mesh.log.textmessage.send.failed %@",
|
||||
comment: "Message Send Failed, not properly connected to %@"), nodeName)
|
||||
MeshLogger.log("🚫 \(logString)")
|
||||
|
|
@ -727,23 +728,19 @@ class BLEManager: NSObject, CBPeripheralDelegate, ObservableObject {
|
|||
return success
|
||||
}
|
||||
|
||||
public func sendWaypoint(destNum: Int64, name: String, wantAck: Bool) -> Bool {
|
||||
public func sendWaypoint(waypoint: Waypoint) -> Bool {
|
||||
|
||||
var success = false
|
||||
let fromNodeNum = connectedPeripheral.num
|
||||
if fromNodeNum <= 0 || (LocationHelper.currentLocation.latitude == LocationHelper.DefaultLocation.latitude && LocationHelper.currentLocation.longitude == LocationHelper.DefaultLocation.longitude) {
|
||||
if waypoint.latitudeI == 373346000 && waypoint.longitudeI == -1220090000 {
|
||||
return false
|
||||
}
|
||||
var waypointPacket = Waypoint()
|
||||
waypointPacket.latitudeI = Int32(LocationHelper.currentLocation.latitude * 1e7)
|
||||
waypointPacket.longitudeI = Int32(LocationHelper.currentLocation.longitude * 1e7)
|
||||
let oneWeekFromNow = Calendar.current.date(byAdding: .day, value: 7, to: Date())
|
||||
waypointPacket.expire = UInt32(oneWeekFromNow!.timeIntervalSince1970)
|
||||
waypointPacket.name = name
|
||||
|
||||
var success = false
|
||||
let fromNodeNum = UInt32(connectedPeripheral.num)
|
||||
var waypointPacket = waypoint
|
||||
var meshPacket = MeshPacket()
|
||||
meshPacket.to = UInt32(destNum)
|
||||
meshPacket.from = 0 // Send 0 as from from phone to device to avoid warning about client trying to set node num
|
||||
meshPacket.wantAck = true//wantAck
|
||||
meshPacket.to = emptyNodeNum
|
||||
meshPacket.from = fromNodeNum
|
||||
meshPacket.wantAck = true
|
||||
var dataMessage = DataMessage()
|
||||
dataMessage.payload = try! waypointPacket.serializedData()
|
||||
dataMessage.portnum = PortNum.waypointApp
|
||||
|
|
@ -758,6 +755,22 @@ class BLEManager: NSObject, CBPeripheralDelegate, ObservableObject {
|
|||
if connectedPeripheral!.peripheral.state == CBPeripheralState.connected {
|
||||
connectedPeripheral.peripheral.writeValue(binaryData, for: TORADIO_characteristic, type: .withResponse)
|
||||
success = true
|
||||
|
||||
let wayPointEntity = getWaypoint(id: Int64(waypoint.id), context: context!)
|
||||
wayPointEntity.id = Int64(waypoint.id)
|
||||
wayPointEntity.name = waypoint.name.count >= 1 ? waypointPacket.name : "Dropped Pin"
|
||||
wayPointEntity.longDescription = waypoint.description_p
|
||||
wayPointEntity.icon = Int64(waypoint.icon)
|
||||
wayPointEntity.latitudeI = waypoint.latitudeI
|
||||
wayPointEntity.longitudeI = waypoint.longitudeI
|
||||
do {
|
||||
try context!.save()
|
||||
print("💾 Updated Waypoint from Waypoint App Packet From: \(fromNodeNum)")
|
||||
} catch {
|
||||
context!.rollback()
|
||||
let nsError = error as NSError
|
||||
print("💥 Error Saving NodeInfoEntity from WAYPOINT_APP \(nsError)")
|
||||
}
|
||||
}
|
||||
return success
|
||||
}
|
||||
|
|
@ -766,9 +779,10 @@ class BLEManager: NSObject, CBPeripheralDelegate, ObservableObject {
|
|||
|
||||
var success = false
|
||||
let fromNodeNum = connectedPeripheral.num
|
||||
if fromNodeNum <= 0 || (LocationHelper.currentLocation.latitude == LocationHelper.DefaultLocation.latitude && LocationHelper.currentLocation.longitude == LocationHelper.DefaultLocation.longitude) {
|
||||
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)
|
||||
|
|
@ -789,9 +803,7 @@ class BLEManager: NSObject, CBPeripheralDelegate, ObservableObject {
|
|||
var dataMessage = DataMessage()
|
||||
dataMessage.payload = try! positionPacket.serializedData()
|
||||
dataMessage.portnum = PortNum.positionApp
|
||||
//if destNum != emptyNodeNum {
|
||||
dataMessage.wantResponse = wantResponse
|
||||
//}
|
||||
dataMessage.wantResponse = wantResponse
|
||||
meshPacket.decoded = dataMessage
|
||||
|
||||
var toRadio: ToRadio!
|
||||
|
|
@ -809,18 +821,13 @@ class BLEManager: NSObject, CBPeripheralDelegate, ObservableObject {
|
|||
}
|
||||
|
||||
@objc func positionTimerFired(timer: Timer) {
|
||||
|
||||
// Check for connected node
|
||||
if connectedPeripheral != nil {
|
||||
|
||||
// Send a position out to the mesh if "share location with the mesh" is enabled in settings
|
||||
if userSettings!.provideLocation {
|
||||
|
||||
let success = sendPosition(destNum: connectedPeripheral.num, wantResponse: false)
|
||||
if !success {
|
||||
|
||||
print("Failed to send positon to device")
|
||||
|
||||
print("Failed to send position to device")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -829,7 +836,7 @@ class BLEManager: NSObject, CBPeripheralDelegate, ObservableObject {
|
|||
public func sendShutdown(fromUser: UserEntity, toUser: UserEntity) -> Bool {
|
||||
|
||||
var adminPacket = AdminMessage()
|
||||
adminPacket.shutdownSeconds = 10
|
||||
adminPacket.shutdownSeconds = 5
|
||||
var meshPacket: MeshPacket = MeshPacket()
|
||||
meshPacket.to = UInt32(toUser.num)
|
||||
meshPacket.from = UInt32(fromUser.num)
|
||||
|
|
@ -852,7 +859,7 @@ class BLEManager: NSObject, CBPeripheralDelegate, ObservableObject {
|
|||
public func sendReboot(fromUser: UserEntity, toUser: UserEntity) -> Bool {
|
||||
|
||||
var adminPacket = AdminMessage()
|
||||
adminPacket.rebootSeconds = 10
|
||||
adminPacket.rebootSeconds = 5
|
||||
|
||||
var meshPacket: MeshPacket = MeshPacket()
|
||||
meshPacket.to = UInt32(connectedPeripheral.num)
|
||||
|
|
@ -860,7 +867,6 @@ class BLEManager: NSObject, CBPeripheralDelegate, ObservableObject {
|
|||
meshPacket.id = UInt32.random(in: UInt32(UInt8.max)..<UInt32.max)
|
||||
meshPacket.priority = MeshPacket.Priority.reliable
|
||||
meshPacket.wantAck = true
|
||||
meshPacket.hopLimit = 0
|
||||
|
||||
var dataMessage = DataMessage()
|
||||
dataMessage.payload = try! adminPacket.serializedData()
|
||||
|
|
@ -1090,17 +1096,17 @@ class BLEManager: NSObject, CBPeripheralDelegate, ObservableObject {
|
|||
return false
|
||||
}
|
||||
|
||||
public func saveUser(config: User, fromUser: UserEntity, toUser: UserEntity) -> Int64 {
|
||||
public func saveUser(config: User, fromUser: UserEntity, toUser: UserEntity, adminIndex: Int32) -> Int64 {
|
||||
|
||||
var adminPacket = AdminMessage()
|
||||
adminPacket.setOwner = config
|
||||
var meshPacket: MeshPacket = MeshPacket()
|
||||
meshPacket.to = UInt32(connectedPeripheral.num)
|
||||
meshPacket.from = 0 //UInt32(connectedPeripheral.num)
|
||||
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
|
||||
meshPacket.hopLimit = 0
|
||||
var dataMessage = DataMessage()
|
||||
dataMessage.payload = try! adminPacket.serializedData()
|
||||
dataMessage.portnum = PortNum.adminApp
|
||||
|
|
@ -1187,17 +1193,19 @@ class BLEManager: NSObject, CBPeripheralDelegate, ObservableObject {
|
|||
return 0
|
||||
}
|
||||
|
||||
public func saveLoRaConfig(config: Config.LoRaConfig, fromUser: UserEntity, toUser: UserEntity) -> Int64 {
|
||||
public func saveLoRaConfig(config: Config.LoRaConfig, fromUser: UserEntity, toUser: UserEntity, adminIndex: Int32) -> Int64 {
|
||||
|
||||
var adminPacket = AdminMessage()
|
||||
adminPacket.setConfig.lora = config
|
||||
var meshPacket: MeshPacket = MeshPacket()
|
||||
meshPacket.to = UInt32(connectedPeripheral.num)
|
||||
meshPacket.from = 0 //UInt32(connectedPeripheral.num)
|
||||
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.wantAck = true
|
||||
meshPacket.hopLimit = 0
|
||||
if adminIndex > 0 {
|
||||
meshPacket.channel = UInt32(adminIndex)
|
||||
}
|
||||
var dataMessage = DataMessage()
|
||||
dataMessage.payload = try! adminPacket.serializedData()
|
||||
dataMessage.portnum = PortNum.adminApp
|
||||
|
|
@ -1240,7 +1248,7 @@ class BLEManager: NSObject, CBPeripheralDelegate, ObservableObject {
|
|||
return 0
|
||||
}
|
||||
|
||||
public func saveWiFiConfig(config: Config.NetworkConfig, fromUser: UserEntity, toUser: UserEntity) -> Int64 {
|
||||
public func saveNetworkConfig(config: Config.NetworkConfig, fromUser: UserEntity, toUser: UserEntity) -> Int64 {
|
||||
|
||||
var adminPacket = AdminMessage()
|
||||
adminPacket.setConfig.network = config
|
||||
|
|
@ -1391,6 +1399,64 @@ class BLEManager: NSObject, CBPeripheralDelegate, ObservableObject {
|
|||
return false
|
||||
}
|
||||
|
||||
public func requestBluetoothConfig(fromUser: UserEntity, toUser: UserEntity, adminIndex: Int32) -> Bool {
|
||||
|
||||
var adminPacket = AdminMessage()
|
||||
adminPacket.getConfigRequest = AdminMessage.ConfigType.bluetoothConfig
|
||||
|
||||
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)
|
||||
|
||||
var dataMessage = DataMessage()
|
||||
dataMessage.payload = try! adminPacket.serializedData()
|
||||
dataMessage.portnum = PortNum.adminApp
|
||||
dataMessage.wantResponse = true
|
||||
|
||||
meshPacket.decoded = dataMessage
|
||||
|
||||
let messageDescription = "🛎️ Requested Bluetooth Config on admin channel \(adminIndex) for node: \(String(connectedPeripheral.num))"
|
||||
|
||||
if sendAdminMessageToRadio(meshPacket: meshPacket, adminDescription: messageDescription, fromUser: fromUser, toUser: toUser) {
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
public func requestLoRaConfig(fromUser: UserEntity, toUser: UserEntity, adminIndex: Int32) -> Bool {
|
||||
|
||||
var adminPacket = AdminMessage()
|
||||
adminPacket.getConfigRequest = AdminMessage.ConfigType.loraConfig
|
||||
|
||||
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)
|
||||
|
||||
var dataMessage = DataMessage()
|
||||
dataMessage.payload = try! adminPacket.serializedData()
|
||||
dataMessage.portnum = PortNum.adminApp
|
||||
dataMessage.wantResponse = true
|
||||
|
||||
meshPacket.decoded = dataMessage
|
||||
|
||||
let messageDescription = "🛎️ Requested LoRa Config on admin channel \(adminIndex) for node: \(String(connectedPeripheral.num))"
|
||||
|
||||
if sendAdminMessageToRadio(meshPacket: meshPacket, adminDescription: messageDescription, fromUser: fromUser, toUser: toUser) {
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
public func saveExternalNotificationModuleConfig(config: ModuleConfig.ExternalNotificationConfig, fromUser: UserEntity, toUser: UserEntity) -> Int64 {
|
||||
|
||||
var adminPacket = AdminMessage()
|
||||
|
|
@ -1542,7 +1608,7 @@ class BLEManager: NSObject, CBPeripheralDelegate, ObservableObject {
|
|||
do {
|
||||
connectedPeripheral.peripheral.writeValue(binaryData, for: TORADIO_characteristic, type: .withResponse)
|
||||
try context!.save()
|
||||
print("⚙️ \(adminDescription)")
|
||||
print(adminDescription)
|
||||
return true
|
||||
} catch {
|
||||
context!.rollback()
|
||||
|
|
|
|||
74
Meshtastic/Helpers/EmojiOnlyTextField.swift
Normal file
74
Meshtastic/Helpers/EmojiOnlyTextField.swift
Normal file
|
|
@ -0,0 +1,74 @@
|
|||
//
|
||||
// EmojiKeyboard.swift
|
||||
// Meshtastic
|
||||
//
|
||||
// Copyright(c) Garth Vander Houwen 1/10/23.
|
||||
//
|
||||
import SwiftUI
|
||||
|
||||
class SwiftUIEmojiTextField: UITextField {
|
||||
|
||||
override func awakeFromNib() {
|
||||
super.awakeFromNib()
|
||||
}
|
||||
|
||||
func setEmoji() {
|
||||
_ = self.textInputMode
|
||||
}
|
||||
|
||||
override var textInputContextIdentifier: String? {
|
||||
return ""
|
||||
}
|
||||
|
||||
override var textInputMode: UITextInputMode? {
|
||||
for mode in UITextInputMode.activeInputModes {
|
||||
if mode.primaryLanguage == "emoji" {
|
||||
self.keyboardType = .default // do not remove this
|
||||
return mode
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
struct EmojiOnlyTextField: UIViewRepresentable {
|
||||
@Binding var text: String
|
||||
var placeholder: String = ""
|
||||
|
||||
func makeUIView(context: Context) -> SwiftUIEmojiTextField {
|
||||
let emojiTextField = SwiftUIEmojiTextField()
|
||||
emojiTextField.placeholder = placeholder
|
||||
emojiTextField.text = text
|
||||
emojiTextField.delegate = context.coordinator
|
||||
return emojiTextField
|
||||
}
|
||||
|
||||
func updateUIView(_ uiView: SwiftUIEmojiTextField, context: Context) {
|
||||
uiView.text = text
|
||||
}
|
||||
|
||||
func makeCoordinator() -> Coordinator {
|
||||
Coordinator(parent: self)
|
||||
}
|
||||
|
||||
class Coordinator: NSObject, UITextFieldDelegate {
|
||||
var parent: EmojiOnlyTextField
|
||||
init(parent: EmojiOnlyTextField) {
|
||||
self.parent = parent
|
||||
}
|
||||
func textFieldDidChangeSelection(_ textField: UITextField) {
|
||||
DispatchQueue.main.async { [weak self] in
|
||||
self?.parent.text = textField.text ?? ""
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//struct EmojiContentView: View {
|
||||
//
|
||||
// @State private var text: String = ""
|
||||
//
|
||||
// var body: some View {
|
||||
// EmojiTextField(text: $text, placeholder: "Enter emoji")
|
||||
// }
|
||||
//}
|
||||
|
|
@ -1,5 +1,24 @@
|
|||
import Foundation
|
||||
import SwiftUI
|
||||
import MapKit
|
||||
|
||||
extension Character {
|
||||
var isEmoji: Bool {
|
||||
guard let scalar = unicodeScalars.first else { return false }
|
||||
return scalar.properties.isEmoji && (scalar.value >= 0x203C || unicodeScalars.count > 1)
|
||||
}
|
||||
}
|
||||
|
||||
extension CLLocationCoordinate2D {
|
||||
/// Returns distance from coordianate in meters.
|
||||
/// - Parameter from: coordinate which will be used as end point.
|
||||
/// - Returns: Returns distance in meters.
|
||||
func distance(from: CLLocationCoordinate2D) -> CLLocationDistance {
|
||||
let from = CLLocation(latitude: from.latitude, longitude: from.longitude)
|
||||
let to = CLLocation(latitude: self.latitude, longitude: self.longitude)
|
||||
return from.distance(from: to)
|
||||
}
|
||||
}
|
||||
|
||||
extension Data {
|
||||
var macAddressString: String {
|
||||
|
|
@ -73,6 +92,10 @@ extension String {
|
|||
return base64url
|
||||
}
|
||||
|
||||
func onlyEmojis() -> Bool {
|
||||
return count > 0 && !contains { !$0.isEmoji }
|
||||
}
|
||||
|
||||
func image(fontSize:CGFloat = 40, bgColor:UIColor = UIColor.clear, imageSize:CGSize? = nil) -> UIImage?
|
||||
{
|
||||
let font = UIFont.systemFont(ofSize: fontSize)
|
||||
|
|
|
|||
|
|
@ -6,7 +6,6 @@ class LocationHelper: NSObject, ObservableObject {
|
|||
|
||||
// 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)
|
||||
|
|
@ -81,7 +80,10 @@ class LocationHelper: NSObject, ObservableObject {
|
|||
|
||||
super.init()
|
||||
locationManager.delegate = self
|
||||
locationManager.desiredAccuracy = kCLLocationAccuracyBest
|
||||
locationManager.desiredAccuracy = kCLLocationAccuracyNearestTenMeters
|
||||
locationManager.pausesLocationUpdatesAutomatically = true
|
||||
locationManager.allowsBackgroundLocationUpdates = true
|
||||
locationManager.activityType = .otherNavigation
|
||||
locationManager.requestWhenInUseAuthorization()
|
||||
locationManager.startUpdatingLocation()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@ func generateMessageMarkdown (message: String) -> String {
|
|||
let matches = detector.matches(in: message, options: [], range: NSRange(location: 0, length: message.utf16.count))
|
||||
var messageWithMarkdown = message
|
||||
if matches.count > 0 {
|
||||
|
||||
|
||||
for match in matches {
|
||||
guard let range = Range(match.range, in: message) else { continue }
|
||||
if match.resultType == .address {
|
||||
|
|
@ -38,295 +38,19 @@ func generateMessageMarkdown (message: String) -> String {
|
|||
|
||||
func localConfig (config: Config, context:NSManagedObjectContext, nodeNum: Int64, nodeLongName: String) {
|
||||
|
||||
// We don't care about any of the Power settings, config is available for everyting else
|
||||
// We don't care about any of the Power settings, config is available for everything else
|
||||
if config.payloadVariant == Config.OneOf_PayloadVariant.bluetooth(config.bluetooth) {
|
||||
|
||||
let logString = String.localizedStringWithFormat(NSLocalizedString("mesh.log.bluetooth.config %@", comment: "Bluetooth config received: %@"), String(nodeNum))
|
||||
MeshLogger.log("📶 \(logString)")
|
||||
|
||||
let fetchNodeInfoRequest: NSFetchRequest<NSFetchRequestResult> = NSFetchRequest.init(entityName: "NodeInfoEntity")
|
||||
fetchNodeInfoRequest.predicate = NSPredicate(format: "num == %lld", Int64(nodeNum))
|
||||
|
||||
do {
|
||||
|
||||
let fetchedNode = try context.fetch(fetchNodeInfoRequest) as! [NodeInfoEntity]
|
||||
// Found a node, save Device Config
|
||||
if !fetchedNode.isEmpty {
|
||||
if fetchedNode[0].bluetoothConfig == nil {
|
||||
let newBluetoothConfig = BluetoothConfigEntity(context: context)
|
||||
newBluetoothConfig.enabled = config.bluetooth.enabled
|
||||
newBluetoothConfig.mode = Int32(config.bluetooth.mode.rawValue)
|
||||
newBluetoothConfig.fixedPin = Int32(config.bluetooth.fixedPin)
|
||||
fetchedNode[0].bluetoothConfig = newBluetoothConfig
|
||||
} else {
|
||||
fetchedNode[0].bluetoothConfig?.enabled = config.bluetooth.enabled
|
||||
fetchedNode[0].bluetoothConfig?.mode = Int32(config.bluetooth.mode.rawValue)
|
||||
fetchedNode[0].bluetoothConfig?.fixedPin = Int32(config.bluetooth.fixedPin)
|
||||
}
|
||||
do {
|
||||
try context.save()
|
||||
print("💾 Updated Bluetooth Config for node number: \(String(nodeNum))")
|
||||
} catch {
|
||||
context.rollback()
|
||||
let nsError = error as NSError
|
||||
print("💥 Error Updating Core Data BluetoothConfigEntity: \(nsError)")
|
||||
}
|
||||
} else {
|
||||
print("💥 No Nodes found in local database matching node number \(nodeNum) unable to save Bluetooth Config")
|
||||
}
|
||||
} catch {
|
||||
let nsError = error as NSError
|
||||
print("💥 Fetching node for core data BluetoothConfigEntity failed: \(nsError)")
|
||||
}
|
||||
}
|
||||
|
||||
if config.payloadVariant == Config.OneOf_PayloadVariant.device(config.device) {
|
||||
|
||||
let logString = String.localizedStringWithFormat(NSLocalizedString("mesh.log.device.config %@", comment: "Device config received: %@"), String(nodeNum))
|
||||
MeshLogger.log("📟 \(logString)")
|
||||
let fetchNodeInfoRequest: NSFetchRequest<NSFetchRequestResult> = NSFetchRequest.init(entityName: "NodeInfoEntity")
|
||||
fetchNodeInfoRequest.predicate = NSPredicate(format: "num == %lld", Int64(nodeNum))
|
||||
|
||||
do {
|
||||
|
||||
let fetchedNode = try context.fetch(fetchNodeInfoRequest) as! [NodeInfoEntity]
|
||||
// Found a node, save Device Config
|
||||
if !fetchedNode.isEmpty {
|
||||
if fetchedNode[0].deviceConfig == nil {
|
||||
let newDeviceConfig = DeviceConfigEntity(context: context)
|
||||
newDeviceConfig.role = Int32(config.device.role.rawValue)
|
||||
newDeviceConfig.serialEnabled = config.device.serialEnabled
|
||||
newDeviceConfig.debugLogEnabled = config.device.debugLogEnabled
|
||||
newDeviceConfig.buttonGpio = Int32(config.device.buttonGpio)
|
||||
newDeviceConfig.buzzerGpio = Int32(config.device.buzzerGpio)
|
||||
fetchedNode[0].deviceConfig = newDeviceConfig
|
||||
} else {
|
||||
fetchedNode[0].deviceConfig?.role = Int32(config.device.role.rawValue)
|
||||
fetchedNode[0].deviceConfig?.serialEnabled = config.device.serialEnabled
|
||||
fetchedNode[0].deviceConfig?.debugLogEnabled = config.device.debugLogEnabled
|
||||
fetchedNode[0].deviceConfig?.buttonGpio = Int32(config.device.buttonGpio)
|
||||
fetchedNode[0].deviceConfig?.buzzerGpio = Int32(config.device.buzzerGpio)
|
||||
}
|
||||
do {
|
||||
try context.save()
|
||||
print("💾 Updated Device Config for node number: \(String(nodeNum))")
|
||||
} catch {
|
||||
context.rollback()
|
||||
let nsError = error as NSError
|
||||
print("💥 Error Updating Core Data DeviceConfigEntity: \(nsError)")
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
let nsError = error as NSError
|
||||
print("💥 Fetching node for core data DeviceConfigEntity failed: \(nsError)")
|
||||
}
|
||||
}
|
||||
|
||||
if config.payloadVariant == Config.OneOf_PayloadVariant.display(config.display) {
|
||||
|
||||
let logString = String.localizedStringWithFormat(NSLocalizedString("mesh.log.display.config %@", comment: "Display config received: %@"), String(nodeNum))
|
||||
MeshLogger.log("🖥️ \(logString)")
|
||||
|
||||
let fetchNodeInfoRequest: NSFetchRequest<NSFetchRequestResult> = NSFetchRequest.init(entityName: "NodeInfoEntity")
|
||||
fetchNodeInfoRequest.predicate = NSPredicate(format: "num == %lld", Int64(nodeNum))
|
||||
|
||||
do {
|
||||
|
||||
let fetchedNode = try context.fetch(fetchNodeInfoRequest) as! [NodeInfoEntity]
|
||||
|
||||
// Found a node, save Device Config
|
||||
if !fetchedNode.isEmpty {
|
||||
|
||||
if fetchedNode[0].displayConfig == nil {
|
||||
|
||||
let newDisplayConfig = DisplayConfigEntity(context: context)
|
||||
newDisplayConfig.gpsFormat = Int32(config.display.gpsFormat.rawValue)
|
||||
newDisplayConfig.screenOnSeconds = Int32(config.display.screenOnSecs)
|
||||
newDisplayConfig.screenCarouselInterval = Int32(config.display.autoScreenCarouselSecs)
|
||||
newDisplayConfig.compassNorthTop = config.display.compassNorthTop
|
||||
newDisplayConfig.flipScreen = config.display.flipScreen
|
||||
newDisplayConfig.oledType = Int32(config.display.oled.rawValue)
|
||||
fetchedNode[0].displayConfig = newDisplayConfig
|
||||
|
||||
} else {
|
||||
|
||||
fetchedNode[0].displayConfig?.gpsFormat = Int32(config.display.gpsFormat.rawValue)
|
||||
fetchedNode[0].displayConfig?.screenOnSeconds = Int32(config.display.screenOnSecs)
|
||||
fetchedNode[0].displayConfig?.screenCarouselInterval = Int32(config.display.autoScreenCarouselSecs)
|
||||
fetchedNode[0].displayConfig?.compassNorthTop = config.display.compassNorthTop
|
||||
fetchedNode[0].displayConfig?.flipScreen = config.display.flipScreen
|
||||
fetchedNode[0].displayConfig?.oledType = Int32(config.display.oled.rawValue)
|
||||
}
|
||||
|
||||
do {
|
||||
|
||||
try context.save()
|
||||
print("💾 Updated Display Config for node number: \(String(nodeNum))")
|
||||
|
||||
} catch {
|
||||
|
||||
context.rollback()
|
||||
|
||||
let nsError = error as NSError
|
||||
print("💥 Error Updating Core Data DisplayConfigEntity: \(nsError)")
|
||||
}
|
||||
} else {
|
||||
|
||||
print("💥 No Nodes found in local database matching node number \(nodeNum) unable to save Display Config")
|
||||
}
|
||||
|
||||
} catch {
|
||||
|
||||
let nsError = error as NSError
|
||||
print("💥 Fetching node for core data DisplayConfigEntity failed: \(nsError)")
|
||||
}
|
||||
}
|
||||
|
||||
if config.payloadVariant == Config.OneOf_PayloadVariant.lora(config.lora) {
|
||||
|
||||
let logString = String.localizedStringWithFormat(NSLocalizedString("mesh.log.lora.config %@", comment: "LoRa config received: %@"), String(nodeNum))
|
||||
MeshLogger.log("📻 \(logString)")
|
||||
|
||||
let fetchNodeInfoRequest: NSFetchRequest<NSFetchRequestResult> = NSFetchRequest.init(entityName: "NodeInfoEntity")
|
||||
fetchNodeInfoRequest.predicate = NSPredicate(format: "num == %lld", Int64(nodeNum))
|
||||
|
||||
do {
|
||||
|
||||
let fetchedNode = try context.fetch(fetchNodeInfoRequest) as! [NodeInfoEntity]
|
||||
// Found a node, save LoRa Config
|
||||
if !fetchedNode.isEmpty {
|
||||
if fetchedNode[0].loRaConfig == nil {
|
||||
let newLoRaConfig = LoRaConfigEntity(context: context)
|
||||
newLoRaConfig.regionCode = Int32(config.lora.region.rawValue)
|
||||
newLoRaConfig.usePreset = config.lora.usePreset
|
||||
newLoRaConfig.modemPreset = Int32(config.lora.modemPreset.rawValue)
|
||||
newLoRaConfig.bandwidth = Int32(config.lora.bandwidth)
|
||||
newLoRaConfig.spreadFactor = Int32(config.lora.spreadFactor)
|
||||
newLoRaConfig.codingRate = Int32(config.lora.codingRate)
|
||||
newLoRaConfig.frequencyOffset = config.lora.frequencyOffset
|
||||
newLoRaConfig.hopLimit = Int32(config.lora.hopLimit)
|
||||
newLoRaConfig.txPower = Int32(config.lora.txPower)
|
||||
newLoRaConfig.txEnabled = config.lora.txEnabled
|
||||
newLoRaConfig.channelNum = Int32(config.lora.channelNum)
|
||||
fetchedNode[0].loRaConfig = newLoRaConfig
|
||||
} else {
|
||||
fetchedNode[0].loRaConfig?.regionCode = Int32(config.lora.region.rawValue)
|
||||
fetchedNode[0].loRaConfig?.usePreset = config.lora.usePreset
|
||||
fetchedNode[0].loRaConfig?.modemPreset = Int32(config.lora.modemPreset.rawValue)
|
||||
fetchedNode[0].loRaConfig?.bandwidth = Int32(config.lora.bandwidth)
|
||||
fetchedNode[0].loRaConfig?.spreadFactor = Int32(config.lora.spreadFactor)
|
||||
fetchedNode[0].loRaConfig?.codingRate = Int32(config.lora.codingRate)
|
||||
fetchedNode[0].loRaConfig?.frequencyOffset = config.lora.frequencyOffset
|
||||
fetchedNode[0].loRaConfig?.hopLimit = Int32(config.lora.hopLimit)
|
||||
fetchedNode[0].loRaConfig?.txPower = Int32(config.lora.txPower)
|
||||
fetchedNode[0].loRaConfig?.txEnabled = config.lora.txEnabled
|
||||
fetchedNode[0].loRaConfig?.channelNum = Int32(config.lora.channelNum)
|
||||
}
|
||||
do {
|
||||
try context.save()
|
||||
print("💾 Updated LoRa Config for node number: \(String(nodeNum))")
|
||||
} catch {
|
||||
context.rollback()
|
||||
let nsError = error as NSError
|
||||
print("💥 Error Updating Core Data LoRaConfigEntity: \(nsError)")
|
||||
}
|
||||
} else {
|
||||
print("💥 No Nodes found in local database matching node number \(nodeNum) unable to save Lora Config")
|
||||
}
|
||||
} catch {
|
||||
let nsError = error as NSError
|
||||
print("💥 Fetching node for core data LoRaConfigEntity failed: \(nsError)")
|
||||
}
|
||||
}
|
||||
|
||||
if config.payloadVariant == Config.OneOf_PayloadVariant.network(config.network) {
|
||||
|
||||
let logString = String.localizedStringWithFormat(NSLocalizedString("mesh.log.network.config %@", comment: "Network config received: %@"), String(nodeNum))
|
||||
MeshLogger.log("🌐 \(logString)")
|
||||
|
||||
let fetchNodeInfoRequest: NSFetchRequest<NSFetchRequestResult> = NSFetchRequest.init(entityName: "NodeInfoEntity")
|
||||
fetchNodeInfoRequest.predicate = NSPredicate(format: "num == %lld", Int64(nodeNum))
|
||||
|
||||
do {
|
||||
|
||||
let fetchedNode = try context.fetch(fetchNodeInfoRequest) as! [NodeInfoEntity]
|
||||
// Found a node, save WiFi Config
|
||||
if !fetchedNode.isEmpty {
|
||||
if fetchedNode[0].networkConfig == nil {
|
||||
let newNetworkConfig = NetworkConfigEntity(context: context)
|
||||
newNetworkConfig.wifiSsid = config.network.wifiSsid
|
||||
newNetworkConfig.wifiPsk = config.network.wifiPsk
|
||||
fetchedNode[0].networkConfig = newNetworkConfig
|
||||
} else {
|
||||
fetchedNode[0].networkConfig?.wifiSsid = config.network.wifiSsid
|
||||
fetchedNode[0].networkConfig?.wifiPsk = config.network.wifiPsk
|
||||
}
|
||||
|
||||
do {
|
||||
try context.save()
|
||||
print("💾 Updated Network Config for node number: \(String(nodeNum))")
|
||||
|
||||
} catch {
|
||||
context.rollback()
|
||||
let nsError = error as NSError
|
||||
print("💥 Error Updating Core Data WiFiConfigEntity: \(nsError)")
|
||||
}
|
||||
} else {
|
||||
print("💥 No Nodes found in local database matching node number \(nodeNum) unable to save Network Config")
|
||||
}
|
||||
} catch {
|
||||
let nsError = error as NSError
|
||||
print("💥 Fetching node for core data NetworkConfigEntity failed: \(nsError)")
|
||||
}
|
||||
}
|
||||
|
||||
if config.payloadVariant == Config.OneOf_PayloadVariant.position(config.position) {
|
||||
|
||||
let logString = String.localizedStringWithFormat(NSLocalizedString("mesh.log.position.config %@", comment: "Positon config received: %@"), String(nodeNum))
|
||||
MeshLogger.log("🗺️ \(logString)")
|
||||
|
||||
let fetchNodeInfoRequest: NSFetchRequest<NSFetchRequestResult> = NSFetchRequest.init(entityName: "NodeInfoEntity")
|
||||
fetchNodeInfoRequest.predicate = NSPredicate(format: "num == %lld", Int64(nodeNum))
|
||||
|
||||
do {
|
||||
|
||||
let fetchedNode = try context.fetch(fetchNodeInfoRequest) as! [NodeInfoEntity]
|
||||
// Found a node, save LoRa Config
|
||||
if !fetchedNode.isEmpty {
|
||||
if fetchedNode[0].positionConfig == nil {
|
||||
let newPositionConfig = PositionConfigEntity(context: context)
|
||||
newPositionConfig.smartPositionEnabled = config.position.positionBroadcastSmartEnabled
|
||||
newPositionConfig.deviceGpsEnabled = config.position.gpsEnabled
|
||||
newPositionConfig.fixedPosition = config.position.fixedPosition
|
||||
newPositionConfig.gpsUpdateInterval = Int32(config.position.gpsUpdateInterval)
|
||||
newPositionConfig.gpsAttemptTime = Int32(config.position.gpsAttemptTime)
|
||||
newPositionConfig.positionBroadcastSeconds = Int32(config.position.positionBroadcastSecs)
|
||||
newPositionConfig.positionFlags = Int32(config.position.positionFlags)
|
||||
fetchedNode[0].positionConfig = newPositionConfig
|
||||
} else {
|
||||
fetchedNode[0].positionConfig?.smartPositionEnabled = config.position.positionBroadcastSmartEnabled
|
||||
fetchedNode[0].positionConfig?.deviceGpsEnabled = config.position.gpsEnabled
|
||||
fetchedNode[0].positionConfig?.fixedPosition = config.position.fixedPosition
|
||||
fetchedNode[0].positionConfig?.gpsUpdateInterval = Int32(config.position.gpsUpdateInterval)
|
||||
fetchedNode[0].positionConfig?.gpsAttemptTime = Int32(config.position.gpsAttemptTime)
|
||||
fetchedNode[0].positionConfig?.positionBroadcastSeconds = Int32(config.position.positionBroadcastSecs)
|
||||
fetchedNode[0].positionConfig?.positionFlags = Int32(config.position.positionFlags)
|
||||
}
|
||||
do {
|
||||
try context.save()
|
||||
print("💾 Updated Position Config for node number: \(String(nodeNum))")
|
||||
} catch {
|
||||
context.rollback()
|
||||
let nsError = error as NSError
|
||||
print("💥 Error Updating Core Data PositionConfigEntity: \(nsError)")
|
||||
}
|
||||
} else {
|
||||
print("💥 No Nodes found in local database matching node number \(nodeNum) unable to save Position Config")
|
||||
}
|
||||
} catch {
|
||||
let nsError = error as NSError
|
||||
print("💥 Fetching node for core data PositionConfigEntity failed: \(nsError)")
|
||||
}
|
||||
upsertBluetoothConfigPacket(config: config, nodeNum: nodeNum, context: context)
|
||||
} else if config.payloadVariant == Config.OneOf_PayloadVariant.device(config.device) {
|
||||
upsertDeviceConfigPacket(config: config, nodeNum: nodeNum, context: context)
|
||||
} else if config.payloadVariant == Config.OneOf_PayloadVariant.display(config.display) {
|
||||
upsertDisplayConfigPacket(config: config, nodeNum: nodeNum, context: context)
|
||||
} else if config.payloadVariant == Config.OneOf_PayloadVariant.lora(config.lora) {
|
||||
upsertLoRaConfigPacket(config: config, nodeNum: nodeNum, context: context)
|
||||
} else if config.payloadVariant == Config.OneOf_PayloadVariant.network(config.network) {
|
||||
upsertNetworkConfigPacket(config: config, nodeNum: nodeNum, context: context)
|
||||
} else if config.payloadVariant == Config.OneOf_PayloadVariant.position(config.position) {
|
||||
upsertPositionConfigPacket(config: config, nodeNum: nodeNum, context: context)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -341,7 +65,7 @@ func moduleConfig (config: ModuleConfig, context:NSManagedObjectContext, nodeNum
|
|||
fetchNodeInfoRequest.predicate = NSPredicate(format: "num == %lld", Int64(nodeNum))
|
||||
|
||||
do {
|
||||
|
||||
|
||||
let fetchedNode = try context.fetch(fetchNodeInfoRequest) as! [NodeInfoEntity]
|
||||
|
||||
// Found a node, save Canned Message Config
|
||||
|
|
@ -350,7 +74,7 @@ func moduleConfig (config: ModuleConfig, context:NSManagedObjectContext, nodeNum
|
|||
if fetchedNode[0].cannedMessageConfig == nil {
|
||||
|
||||
let newCannedMessageConfig = CannedMessageConfigEntity(context: context)
|
||||
|
||||
|
||||
newCannedMessageConfig.enabled = config.cannedMessage.enabled
|
||||
newCannedMessageConfig.sendBell = config.cannedMessage.sendBell
|
||||
newCannedMessageConfig.rotary1Enabled = config.cannedMessage.rotary1Enabled
|
||||
|
|
@ -361,7 +85,7 @@ func moduleConfig (config: ModuleConfig, context:NSManagedObjectContext, nodeNum
|
|||
newCannedMessageConfig.inputbrokerEventCw = Int32(config.cannedMessage.inputbrokerEventCw.rawValue)
|
||||
newCannedMessageConfig.inputbrokerEventCcw = Int32(config.cannedMessage.inputbrokerEventCcw.rawValue)
|
||||
newCannedMessageConfig.inputbrokerEventPress = Int32(config.cannedMessage.inputbrokerEventPress.rawValue)
|
||||
|
||||
|
||||
fetchedNode[0].cannedMessageConfig = newCannedMessageConfig
|
||||
|
||||
} else {
|
||||
|
|
@ -405,7 +129,7 @@ func moduleConfig (config: ModuleConfig, context:NSManagedObjectContext, nodeNum
|
|||
fetchNodeInfoRequest.predicate = NSPredicate(format: "num == %lld", Int64(nodeNum))
|
||||
|
||||
do {
|
||||
|
||||
|
||||
let fetchedNode = try context.fetch(fetchNodeInfoRequest) as! [NodeInfoEntity]
|
||||
// Found a node, save External Notificaitone Config
|
||||
if !fetchedNode.isEmpty {
|
||||
|
|
@ -471,7 +195,7 @@ func moduleConfig (config: ModuleConfig, context:NSManagedObjectContext, nodeNum
|
|||
fetchNodeInfoRequest.predicate = NSPredicate(format: "num == %lld", Int64(nodeNum))
|
||||
|
||||
do {
|
||||
|
||||
|
||||
let fetchedNode = try context.fetch(fetchNodeInfoRequest) as! [NodeInfoEntity]
|
||||
// Found a node, save MQTT Config
|
||||
if !fetchedNode.isEmpty {
|
||||
|
|
@ -519,7 +243,7 @@ func moduleConfig (config: ModuleConfig, context:NSManagedObjectContext, nodeNum
|
|||
fetchNodeInfoRequest.predicate = NSPredicate(format: "num == %lld", Int64(nodeNum))
|
||||
|
||||
do {
|
||||
|
||||
|
||||
let fetchedNode = try context.fetch(fetchNodeInfoRequest) as! [NodeInfoEntity]
|
||||
// Found a node, save Device Config
|
||||
if !fetchedNode.isEmpty {
|
||||
|
|
@ -550,7 +274,7 @@ func moduleConfig (config: ModuleConfig, context:NSManagedObjectContext, nodeNum
|
|||
print("💥 Fetching node for core data RangeTestConfigEntity failed: \(nsError)")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
if config.payloadVariant == ModuleConfig.OneOf_PayloadVariant.serial(config.serial) {
|
||||
|
||||
let logString = String.localizedStringWithFormat(NSLocalizedString("mesh.log.serial.config %@", comment: "Serial module config received: %@"), String(nodeNum))
|
||||
|
|
@ -560,7 +284,7 @@ func moduleConfig (config: ModuleConfig, context:NSManagedObjectContext, nodeNum
|
|||
fetchNodeInfoRequest.predicate = NSPredicate(format: "num == %lld", Int64(nodeNum))
|
||||
|
||||
do {
|
||||
|
||||
|
||||
let fetchedNode = try context.fetch(fetchNodeInfoRequest) as! [NodeInfoEntity]
|
||||
|
||||
// Found a node, save Device Config
|
||||
|
|
@ -591,11 +315,11 @@ func moduleConfig (config: ModuleConfig, context:NSManagedObjectContext, nodeNum
|
|||
do {
|
||||
try context.save()
|
||||
print("💾 Updated Serial Module Config for node number: \(String(nodeNum))")
|
||||
|
||||
|
||||
} catch {
|
||||
|
||||
|
||||
context.rollback()
|
||||
|
||||
|
||||
let nsError = error as NSError
|
||||
print("💥 Error Updating Core Data SerialConfigEntity: \(nsError)")
|
||||
}
|
||||
|
|
@ -621,7 +345,7 @@ func moduleConfig (config: ModuleConfig, context:NSManagedObjectContext, nodeNum
|
|||
fetchNodeInfoRequest.predicate = NSPredicate(format: "num == %lld", Int64(nodeNum))
|
||||
|
||||
do {
|
||||
|
||||
|
||||
let fetchedNode = try context.fetch(fetchNodeInfoRequest) as! [NodeInfoEntity]
|
||||
// Found a node, save Telemetry Config
|
||||
if !fetchedNode.isEmpty {
|
||||
|
|
@ -650,7 +374,7 @@ func moduleConfig (config: ModuleConfig, context:NSManagedObjectContext, nodeNum
|
|||
do {
|
||||
try context.save()
|
||||
print("💾 Updated Telemetry Module Config for node number: \(String(nodeNum))")
|
||||
|
||||
|
||||
} catch {
|
||||
context.rollback()
|
||||
let nsError = error as NSError
|
||||
|
|
@ -675,7 +399,7 @@ func myInfoPacket (myInfo: MyNodeInfo, peripheralId: String, context: NSManagedO
|
|||
|
||||
let fetchMyInfoRequest: NSFetchRequest<NSFetchRequestResult> = NSFetchRequest.init(entityName: "MyInfoEntity")
|
||||
fetchMyInfoRequest.predicate = NSPredicate(format: "myNodeNum == %lld", Int64(myInfo.myNodeNum))
|
||||
|
||||
|
||||
do {
|
||||
let fetchedMyInfo = try context.fetch(fetchMyInfoRequest) as! [MyInfoEntity]
|
||||
// Not Found Insert
|
||||
|
|
@ -705,7 +429,7 @@ func myInfoPacket (myInfo: MyNodeInfo, peripheralId: String, context: NSManagedO
|
|||
print("💥 Error Inserting New Core Data MyInfoEntity: \(nsError)")
|
||||
}
|
||||
} else {
|
||||
|
||||
|
||||
fetchedMyInfo[0].peripheralId = peripheralId
|
||||
fetchedMyInfo[0].myNodeNum = Int64(myInfo.myNodeNum)
|
||||
fetchedMyInfo[0].hasGps = myInfo.hasGps_p
|
||||
|
|
@ -737,7 +461,7 @@ func myInfoPacket (myInfo: MyNodeInfo, peripheralId: String, context: NSManagedO
|
|||
func channelPacket (channel: Channel, fromNum: Int64, context: NSManagedObjectContext) {
|
||||
|
||||
if channel.isInitialized && channel.hasSettings && channel.role != Channel.Role.disabled {
|
||||
|
||||
|
||||
let logString = String.localizedStringWithFormat(NSLocalizedString("mesh.log.channel.received %d %@", comment: "Channel %d received from: %@"), channel.index, String(fromNum))
|
||||
MeshLogger.log("🎛️ \(logString)")
|
||||
|
||||
|
|
@ -763,6 +487,9 @@ func channelPacket (channel: Channel, fromNum: Int64, context: NSManagedObjectCo
|
|||
mutableChannels.add(newChannel)
|
||||
}
|
||||
fetchedMyInfo[0].channels = mutableChannels.copy() as? NSOrderedSet
|
||||
if newChannel.name?.lowercased() == "admin" {
|
||||
fetchedMyInfo[0].adminIndex = newChannel.index
|
||||
}
|
||||
do {
|
||||
try context.save()
|
||||
} catch {
|
||||
|
|
@ -780,6 +507,45 @@ func channelPacket (channel: Channel, fromNum: Int64, context: NSManagedObjectCo
|
|||
}
|
||||
}
|
||||
|
||||
func deviceMetadataPacket (metadata: DeviceMetadata, fromNum: Int64, context: NSManagedObjectContext) {
|
||||
|
||||
if metadata.isInitialized {
|
||||
let logString = String.localizedStringWithFormat(NSLocalizedString("mesh.log.device.metadata.received %@", comment: "Device Metadata admin message received from: %@"), String(fromNum))
|
||||
MeshLogger.log("🏷️ \(logString)")
|
||||
|
||||
let fetchedNodeRequest: NSFetchRequest<NSFetchRequestResult> = NSFetchRequest.init(entityName: "NodeInfoEntity")
|
||||
fetchedNodeRequest.predicate = NSPredicate(format: "num == %lld", fromNum)
|
||||
|
||||
do {
|
||||
|
||||
let fetchedNode = try context.fetch(fetchedNodeRequest) as! [NodeInfoEntity]
|
||||
if fetchedNode.count > 0 {
|
||||
let newMetadata = DeviceMetadataEntity(context: context)
|
||||
newMetadata.firmwareVersion = metadata.firmwareVersion
|
||||
newMetadata.deviceStateVersion = Int32(metadata.deviceStateVersion)
|
||||
newMetadata.canShutdown = metadata.canShutdown
|
||||
newMetadata.hasWifi = metadata.hasWifi_p
|
||||
newMetadata.hasBluetooth = metadata.hasBluetooth_p
|
||||
newMetadata.hasEthernet = metadata.hasEthernet_p
|
||||
newMetadata.role = Int32(metadata.role.rawValue)
|
||||
newMetadata.positionFlags = Int32(metadata.positionFlags)
|
||||
fetchedNode[0].metadata = newMetadata
|
||||
|
||||
do {
|
||||
try context.save()
|
||||
} catch {
|
||||
print("Failed to save device metadata")
|
||||
}
|
||||
print("💾 Updated Device Metadata from Admin App Packet For: \(fromNum)")
|
||||
}
|
||||
} catch {
|
||||
context.rollback()
|
||||
let nsError = error as NSError
|
||||
print("💥 Error Saving MyInfo Channel from ADMIN_APP \(nsError)")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func nodeInfoPacket (nodeInfo: NodeInfo, channel: UInt32, context: NSManagedObjectContext) -> NodeInfoEntity? {
|
||||
|
||||
let logString = String.localizedStringWithFormat(NSLocalizedString("mesh.log.nodeinfo.received %@", comment: "Node info received for: %@"), String(nodeInfo.num))
|
||||
|
|
@ -787,13 +553,13 @@ func nodeInfoPacket (nodeInfo: NodeInfo, channel: UInt32, context: NSManagedObje
|
|||
|
||||
let fetchNodeInfoRequest: NSFetchRequest<NSFetchRequestResult> = NSFetchRequest.init(entityName: "NodeInfoEntity")
|
||||
fetchNodeInfoRequest.predicate = NSPredicate(format: "num == %lld", Int64(nodeInfo.num))
|
||||
|
||||
|
||||
do {
|
||||
|
||||
|
||||
let fetchedNode = try context.fetch(fetchNodeInfoRequest) as! [NodeInfoEntity]
|
||||
// Not Found Insert
|
||||
if fetchedNode.isEmpty && nodeInfo.hasUser {
|
||||
|
||||
|
||||
let newNode = NodeInfoEntity(context: context)
|
||||
newNode.id = Int64(nodeInfo.num)
|
||||
newNode.num = Int64(nodeInfo.num)
|
||||
|
|
@ -822,8 +588,9 @@ func nodeInfoPacket (nodeInfo: NodeInfo, channel: UInt32, context: NSManagedObje
|
|||
newUser.hwModel = String(describing: nodeInfo.user.hwModel).uppercased()
|
||||
newNode.user = newUser
|
||||
}
|
||||
|
||||
if nodeInfo.position.latitudeI > 0 || nodeInfo.position.longitudeI > 0 {
|
||||
|
||||
if nodeInfo.position.longitudeI > 0 || nodeInfo.position.latitudeI > 0 && (nodeInfo.position.latitudeI != 373346000 && nodeInfo.position.longitudeI != -1220090000)
|
||||
{
|
||||
let position = PositionEntity(context: context)
|
||||
position.seqNo = Int32(nodeInfo.position.seqNumber)
|
||||
position.latitudeI = nodeInfo.position.latitudeI
|
||||
|
|
@ -837,13 +604,13 @@ func nodeInfoPacket (nodeInfo: NodeInfo, channel: UInt32, context: NSManagedObje
|
|||
newPostions.append(position)
|
||||
newNode.positions? = NSOrderedSet(array: newPostions)
|
||||
}
|
||||
|
||||
|
||||
// Look for a MyInfo
|
||||
let fetchMyInfoRequest: NSFetchRequest<NSFetchRequestResult> = NSFetchRequest.init(entityName: "MyInfoEntity")
|
||||
fetchMyInfoRequest.predicate = NSPredicate(format: "myNodeNum == %lld", Int64(nodeInfo.num))
|
||||
|
||||
|
||||
do {
|
||||
|
||||
|
||||
let fetchedMyInfo = try context.fetch(fetchMyInfoRequest) as! [MyInfoEntity]
|
||||
if fetchedMyInfo.count > 0 {
|
||||
newNode.myInfo = fetchedMyInfo[0]
|
||||
|
|
@ -860,16 +627,15 @@ func nodeInfoPacket (nodeInfo: NodeInfo, channel: UInt32, context: NSManagedObje
|
|||
print("💥 Fetch MyInfo Error")
|
||||
}
|
||||
} else if nodeInfo.hasUser && nodeInfo.num > 0 {
|
||||
|
||||
|
||||
fetchedNode[0].id = Int64(nodeInfo.num)
|
||||
fetchedNode[0].num = Int64(nodeInfo.num)
|
||||
fetchedNode[0].lastHeard = Date(timeIntervalSince1970: TimeInterval(Int64(nodeInfo.lastHeard)))
|
||||
fetchedNode[0].snr = nodeInfo.snr
|
||||
fetchedNode[0].channel = Int32(channel)
|
||||
|
||||
|
||||
if nodeInfo.hasUser {
|
||||
|
||||
|
||||
fetchedNode[0].user!.userId = nodeInfo.user.id
|
||||
fetchedNode[0].user!.num = Int64(nodeInfo.num)
|
||||
fetchedNode[0].user!.longName = nodeInfo.user.longName
|
||||
|
|
@ -877,7 +643,7 @@ func nodeInfoPacket (nodeInfo: NodeInfo, channel: UInt32, context: NSManagedObje
|
|||
fetchedNode[0].user!.macaddr = nodeInfo.user.macaddr
|
||||
fetchedNode[0].user!.hwModel = String(describing: nodeInfo.user.hwModel).uppercased()
|
||||
}
|
||||
|
||||
|
||||
if nodeInfo.hasDeviceMetrics {
|
||||
|
||||
let newTelemetry = TelemetryEntity(context: context)
|
||||
|
|
@ -890,21 +656,26 @@ func nodeInfoPacket (nodeInfo: NodeInfo, channel: UInt32, context: NSManagedObje
|
|||
}
|
||||
|
||||
if nodeInfo.hasPosition {
|
||||
|
||||
let position = PositionEntity(context: context)
|
||||
position.latitudeI = nodeInfo.position.latitudeI
|
||||
position.longitudeI = nodeInfo.position.longitudeI
|
||||
position.altitude = nodeInfo.position.altitude
|
||||
position.satsInView = Int32(nodeInfo.position.satsInView)
|
||||
position.time = Date(timeIntervalSince1970: TimeInterval(Int64(nodeInfo.position.time)))
|
||||
let mutablePositions = fetchedNode[0].positions!.mutableCopy() as! NSMutableOrderedSet
|
||||
fetchedNode[0].positions = mutablePositions.copy() as? NSOrderedSet
|
||||
|
||||
|
||||
if nodeInfo.position.longitudeI > 0 || nodeInfo.position.latitudeI > 0 && (nodeInfo.position.latitudeI != 373346000 && nodeInfo.position.longitudeI != -1220090000) {
|
||||
|
||||
let position = PositionEntity(context: context)
|
||||
position.latitudeI = nodeInfo.position.latitudeI
|
||||
position.longitudeI = nodeInfo.position.longitudeI
|
||||
position.altitude = nodeInfo.position.altitude
|
||||
position.satsInView = Int32(nodeInfo.position.satsInView)
|
||||
position.time = Date(timeIntervalSince1970: TimeInterval(Int64(nodeInfo.position.time)))
|
||||
let mutablePositions = fetchedNode[0].positions!.mutableCopy() as! NSMutableOrderedSet
|
||||
fetchedNode[0].positions = mutablePositions.copy() as? NSOrderedSet
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
// Look for a MyInfo
|
||||
let fetchMyInfoRequest: NSFetchRequest<NSFetchRequestResult> = NSFetchRequest.init(entityName: "MyInfoEntity")
|
||||
fetchMyInfoRequest.predicate = NSPredicate(format: "myNodeNum == %lld", Int64(nodeInfo.num))
|
||||
|
||||
|
||||
do {
|
||||
let fetchedMyInfo = try context.fetch(fetchMyInfoRequest) as! [MyInfoEntity]
|
||||
if fetchedMyInfo.count > 0 {
|
||||
|
|
@ -930,17 +701,17 @@ func nodeInfoPacket (nodeInfo: NodeInfo, channel: UInt32, context: NSManagedObje
|
|||
}
|
||||
|
||||
func nodeInfoAppPacket (packet: MeshPacket, context: NSManagedObjectContext) {
|
||||
|
||||
|
||||
let logString = String.localizedStringWithFormat(NSLocalizedString("mesh.log.nodeinfo.received %@", comment: "Node info received for: %@"), String(packet.from))
|
||||
MeshLogger.log("📟 \(logString)")
|
||||
|
||||
let fetchNodeInfoAppRequest: NSFetchRequest<NSFetchRequestResult> = NSFetchRequest.init(entityName: "NodeInfoEntity")
|
||||
fetchNodeInfoAppRequest.predicate = NSPredicate(format: "num == %lld", Int64(packet.from))
|
||||
|
||||
|
||||
do {
|
||||
|
||||
|
||||
let fetchedNode = try context.fetch(fetchNodeInfoAppRequest) as? [NodeInfoEntity] ?? []
|
||||
|
||||
|
||||
if fetchedNode.count == 1 {
|
||||
fetchedNode[0].id = Int64(packet.from)
|
||||
fetchedNode[0].num = Int64(packet.from)
|
||||
|
|
@ -988,10 +759,12 @@ func adminAppPacket (packet: MeshPacket, context: NSManagedObjectContext) {
|
|||
|
||||
if let adminMessage = try? AdminMessage(serializedData: packet.decoded.payload) {
|
||||
|
||||
|
||||
|
||||
if adminMessage.payloadVariant == AdminMessage.OneOf_PayloadVariant.getCannedMessageModuleMessagesResponse(adminMessage.getCannedMessageModuleMessagesResponse) {
|
||||
|
||||
if let cmmc = try? CannedMessageModuleConfig(serializedData: packet.decoded.payload) {
|
||||
|
||||
|
||||
if !cmmc.messages.isEmpty {
|
||||
|
||||
let logString = String.localizedStringWithFormat(NSLocalizedString("mesh.log.cannedmessages.messages.received %@", comment: "Canned Messages Messages Received For: %@"), String(packet.from))
|
||||
|
|
@ -1022,69 +795,35 @@ func adminAppPacket (packet: MeshPacket, context: NSManagedObjectContext) {
|
|||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
else if adminMessage.payloadVariant == AdminMessage.OneOf_PayloadVariant.getChannelResponse(adminMessage.getChannelResponse) {
|
||||
|
||||
} else if adminMessage.payloadVariant == AdminMessage.OneOf_PayloadVariant.getChannelResponse(adminMessage.getChannelResponse) {
|
||||
channelPacket(channel: adminMessage.getChannelResponse, fromNum: Int64(packet.from), context: context)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func positionPacket (packet: MeshPacket, context: NSManagedObjectContext) {
|
||||
|
||||
let logString = String.localizedStringWithFormat(NSLocalizedString("mesh.log.position.received %@", comment: "Position Packet received from node: %@"), String(packet.from))
|
||||
MeshLogger.log("📍 \(logString)")
|
||||
|
||||
let fetchNodePositionRequest: NSFetchRequest<NSFetchRequestResult> = NSFetchRequest.init(entityName: "NodeInfoEntity")
|
||||
fetchNodePositionRequest.predicate = NSPredicate(format: "num == %lld", Int64(packet.from))
|
||||
|
||||
do {
|
||||
|
||||
if let positionMessage = try? Position(serializedData: packet.decoded.payload) {
|
||||
// Don't save empty position packets
|
||||
if positionMessage.longitudeI > 0 || positionMessage.latitudeI > 0 {
|
||||
let fetchedNode = try context.fetch(fetchNodePositionRequest) as! [NodeInfoEntity]
|
||||
if fetchedNode.count == 1 {
|
||||
|
||||
let position = PositionEntity(context: context)
|
||||
position.snr = packet.rxSnr
|
||||
position.seqNo = Int32(positionMessage.seqNumber)
|
||||
position.latitudeI = positionMessage.latitudeI
|
||||
position.longitudeI = positionMessage.longitudeI
|
||||
position.altitude = positionMessage.altitude
|
||||
position.satsInView = Int32(positionMessage.satsInView)
|
||||
position.speed = Int32(positionMessage.groundSpeed)
|
||||
position.heading = Int32(positionMessage.groundTrack)
|
||||
if positionMessage.timestamp != 0 {
|
||||
position.time = Date(timeIntervalSince1970: TimeInterval(Int64(positionMessage.timestamp)))
|
||||
} else {
|
||||
position.time = Date(timeIntervalSince1970: TimeInterval(Int64(positionMessage.time)))
|
||||
}
|
||||
let mutablePositions = fetchedNode[0].positions!.mutableCopy() as! NSMutableOrderedSet
|
||||
mutablePositions.add(position)
|
||||
fetchedNode[0].id = Int64(packet.from)
|
||||
fetchedNode[0].num = Int64(packet.from)
|
||||
fetchedNode[0].lastHeard = Date(timeIntervalSince1970: TimeInterval(Int64(positionMessage.time)))
|
||||
fetchedNode[0].snr = packet.rxSnr
|
||||
fetchedNode[0].positions = mutablePositions.copy() as? NSOrderedSet
|
||||
do {
|
||||
try context.save()
|
||||
print("💾 Updated Node Position Coordinates, SNR and Time from Position App Packet For: \(fetchedNode[0].num)")
|
||||
} catch {
|
||||
context.rollback()
|
||||
let nsError = error as NSError
|
||||
print("💥 Error Saving NodeInfoEntity from POSITION_APP \(nsError)")
|
||||
}
|
||||
} else if adminMessage.payloadVariant == AdminMessage.OneOf_PayloadVariant.getDeviceMetadataResponse(adminMessage.getDeviceMetadataResponse) {
|
||||
deviceMetadataPacket(metadata: adminMessage.getDeviceMetadataResponse, fromNum: Int64(packet.from), context: context)
|
||||
|
||||
} else if adminMessage.payloadVariant == AdminMessage.OneOf_PayloadVariant.getConfigResponse(adminMessage.getConfigResponse) {
|
||||
if let config = try? Config(serializedData: packet.decoded.payload) {
|
||||
|
||||
if config.payloadVariant == Config.OneOf_PayloadVariant.bluetooth(config.bluetooth) {
|
||||
upsertBluetoothConfigPacket(config: config, nodeNum: Int64(packet.from), context: context)
|
||||
|
||||
} else if config.payloadVariant == Config.OneOf_PayloadVariant.device(config.device) {
|
||||
upsertDeviceConfigPacket(config: config, nodeNum: Int64(packet.from), context: context)
|
||||
|
||||
} else if config.payloadVariant == Config.OneOf_PayloadVariant.lora(config.lora) {
|
||||
upsertLoRaConfigPacket(config: config, nodeNum: Int64(packet.from), context: context)
|
||||
|
||||
} else if config.payloadVariant == Config.OneOf_PayloadVariant.network(config.network) {
|
||||
upsertNetworkConfigPacket(config: config, nodeNum: Int64(packet.from), context: context)
|
||||
|
||||
} else if config.payloadVariant == Config.OneOf_PayloadVariant.position(config.position) {
|
||||
upsertPositionConfigPacket(config: config, nodeNum: Int64(packet.from), context: context)
|
||||
|
||||
}
|
||||
} else {
|
||||
print("💥 Empty POSITION_APP Packet")
|
||||
print(try! packet.jsonString())
|
||||
}
|
||||
} else {
|
||||
MeshLogger.log("🕸️ MESH PACKET received for Admin App \(try! packet.decoded.jsonString())")
|
||||
}
|
||||
} catch {
|
||||
print("💥 Error Deserializing POSITION_APP packet.")
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -1097,10 +836,10 @@ func routingPacket (packet: MeshPacket, connectedNodeNum: Int64, context: NSMana
|
|||
let routingErrorString = routingError?.display ?? NSLocalizedString("unknown", comment: "")
|
||||
let logString = String.localizedStringWithFormat(NSLocalizedString("mesh.log.routing.message %@ %@", comment: "Routing received for RequestID: %@ Ack Status: %@"), String(packet.decoded.requestID), routingErrorString)
|
||||
MeshLogger.log("🕸️ \(logString)")
|
||||
|
||||
|
||||
let fetchMessageRequest: NSFetchRequest<NSFetchRequestResult> = NSFetchRequest.init(entityName: "MessageEntity")
|
||||
fetchMessageRequest.predicate = NSPredicate(format: "messageId == %lld", Int64(packet.decoded.requestID))
|
||||
|
||||
|
||||
do {
|
||||
let fetchedMessage = try context.fetch(fetchMessageRequest) as? [MessageEntity]
|
||||
if fetchedMessage?.count ?? 0 > 0 {
|
||||
|
|
@ -1130,14 +869,14 @@ func routingPacket (packet: MeshPacket, connectedNodeNum: Int64, context: NSMana
|
|||
if fetchedMyInfo?.count ?? 0 > 0 {
|
||||
|
||||
for ch in fetchedMyInfo![0].channels!.array as! [ChannelEntity] {
|
||||
|
||||
|
||||
if ch.index == packet.channel {
|
||||
ch.objectWillChange.send()
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -1153,7 +892,7 @@ func routingPacket (packet: MeshPacket, connectedNodeNum: Int64, context: NSMana
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
func telemetryPacket(packet: MeshPacket, connectedNode: Int64, context: NSManagedObjectContext) {
|
||||
|
||||
if let telemetryMessage = try? Telemetry(serializedData: packet.decoded.payload) {
|
||||
|
|
@ -1168,9 +907,9 @@ func telemetryPacket(packet: MeshPacket, connectedNode: Int64, context: NSManage
|
|||
|
||||
let fetchNodeTelemetryRequest: NSFetchRequest<NSFetchRequestResult> = NSFetchRequest.init(entityName: "NodeInfoEntity")
|
||||
fetchNodeTelemetryRequest.predicate = NSPredicate(format: "num == %lld", Int64(packet.from))
|
||||
|
||||
|
||||
do {
|
||||
|
||||
|
||||
let fetchedNode = try context.fetch(fetchNodeTelemetryRequest) as! [NodeInfoEntity]
|
||||
if fetchedNode.count == 1 {
|
||||
if telemetryMessage.variant == Telemetry.OneOf_Variant.deviceMetrics(telemetryMessage.deviceMetrics) {
|
||||
|
|
@ -1213,16 +952,16 @@ func telemetryPacket(packet: MeshPacket, connectedNode: Int64, context: NSManage
|
|||
}
|
||||
|
||||
func textMessageAppPacket(packet: MeshPacket, connectedNode: Int64, context: NSManagedObjectContext) {
|
||||
|
||||
|
||||
if let messageText = String(bytes: packet.decoded.payload, encoding: .utf8) {
|
||||
|
||||
|
||||
MeshLogger.log("💬 \(NSLocalizedString("mesh.log.textmessage.received", comment: "Message received from the text message app"))")
|
||||
|
||||
let messageUsers: NSFetchRequest<NSFetchRequestResult> = NSFetchRequest.init(entityName: "UserEntity")
|
||||
messageUsers.predicate = NSPredicate(format: "num IN %@", [packet.to, packet.from])
|
||||
|
||||
|
||||
do {
|
||||
|
||||
|
||||
let fetchedUsers = try context.fetch(messageUsers) as! [UserEntity]
|
||||
let newMessage = MessageEntity(context: context)
|
||||
newMessage.messageId = Int64(packet.id)
|
||||
|
|
@ -1235,7 +974,7 @@ func textMessageAppPacket(packet: MeshPacket, connectedNode: Int64, context: NSM
|
|||
if packet.decoded.replyID > 0 {
|
||||
newMessage.replyID = Int64(packet.decoded.replyID)
|
||||
}
|
||||
|
||||
|
||||
if fetchedUsers.first(where: { $0.num == packet.to }) != nil && packet.to != 4294967295 {
|
||||
newMessage.toUser = fetchedUsers.first(where: { $0.num == packet.to })
|
||||
}
|
||||
|
|
@ -1244,14 +983,14 @@ func textMessageAppPacket(packet: MeshPacket, connectedNode: Int64, context: NSM
|
|||
}
|
||||
newMessage.messagePayload = messageText
|
||||
newMessage.messagePayloadMarkdown = generateMessageMarkdown(message: messageText)
|
||||
|
||||
|
||||
newMessage.fromUser?.objectWillChange.send()
|
||||
newMessage.toUser?.objectWillChange.send()
|
||||
|
||||
var messageSaved = false
|
||||
|
||||
|
||||
do {
|
||||
|
||||
|
||||
try context.save()
|
||||
print("💾 Saved a new message for \(newMessage.messageId)")
|
||||
messageSaved = true
|
||||
|
|
@ -1274,7 +1013,7 @@ func textMessageAppPacket(packet: MeshPacket, connectedNode: Int64, context: NSM
|
|||
|
||||
let fetchMyInfoRequest: NSFetchRequest<NSFetchRequestResult> = NSFetchRequest.init(entityName: "MyInfoEntity")
|
||||
fetchMyInfoRequest.predicate = NSPredicate(format: "myNodeNum == %lld", Int64(connectedNode))
|
||||
|
||||
|
||||
do {
|
||||
let fetchedMyInfo = try context.fetch(fetchMyInfoRequest) as! [MyInfoEntity]
|
||||
for channel in (fetchedMyInfo[0].channels?.array ?? []) as? [ChannelEntity] ?? [] {
|
||||
|
|
@ -1311,3 +1050,63 @@ func textMessageAppPacket(packet: MeshPacket, connectedNode: Int64, context: NSM
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
func waypointPacket (packet: MeshPacket, context: NSManagedObjectContext) {
|
||||
|
||||
let logString = String.localizedStringWithFormat(NSLocalizedString("mesh.log.waypoint.received %@", comment: "Waypoint Packet received from node: %@"), String(packet.from))
|
||||
MeshLogger.log("📍 \(logString)")
|
||||
|
||||
let fetchWaypointRequest: NSFetchRequest<NSFetchRequestResult> = NSFetchRequest.init(entityName: "WaypointEntity")
|
||||
fetchWaypointRequest.predicate = NSPredicate(format: "id == %lld", Int64(packet.id))
|
||||
|
||||
do {
|
||||
|
||||
if let waypointMessage = try? Waypoint(serializedData: packet.decoded.payload) {
|
||||
|
||||
let fetchedWaypoint = try context.fetch(fetchWaypointRequest) as! [WaypointEntity]
|
||||
if fetchedWaypoint.isEmpty {
|
||||
let waypoint = WaypointEntity(context: context)
|
||||
|
||||
waypoint.id = Int64(packet.id)
|
||||
waypoint.name = waypointMessage.name
|
||||
waypoint.longDescription = waypointMessage.description_p
|
||||
waypoint.latitudeI = waypointMessage.latitudeI
|
||||
waypoint.longitudeI = waypointMessage.longitudeI
|
||||
waypoint.icon = Int64(waypointMessage.icon)
|
||||
waypoint.locked = Int64(waypointMessage.lockedTo)
|
||||
if waypointMessage.expire > 0 {
|
||||
waypoint.expire = Date(timeIntervalSince1970: TimeInterval(Int64(waypointMessage.expire)))
|
||||
}
|
||||
do {
|
||||
try context.save()
|
||||
print("💾 Updated Node Waypoint App Packet For: \(waypoint.id)")
|
||||
} catch {
|
||||
context.rollback()
|
||||
let nsError = error as NSError
|
||||
print("💥 Error Saving WaypointEntity from WAYPOINT_APP \(nsError)")
|
||||
}
|
||||
} else {
|
||||
fetchedWaypoint[0].id = Int64(packet.id)
|
||||
fetchedWaypoint[0].name = waypointMessage.name
|
||||
fetchedWaypoint[0].longDescription = waypointMessage.description_p
|
||||
fetchedWaypoint[0].latitudeI = waypointMessage.latitudeI
|
||||
fetchedWaypoint[0].longitudeI = waypointMessage.longitudeI
|
||||
fetchedWaypoint[0].icon = Int64(waypointMessage.icon)
|
||||
fetchedWaypoint[0].locked = Int64(waypointMessage.lockedTo)
|
||||
if waypointMessage.expire > 0 {
|
||||
fetchedWaypoint[0].expire = Date(timeIntervalSince1970: TimeInterval(Int64(waypointMessage.expire)))
|
||||
}
|
||||
do {
|
||||
try context.save()
|
||||
print("💾 Updated Node Waypoint App Packet For: \(fetchedWaypoint[0].id)")
|
||||
} catch {
|
||||
context.rollback()
|
||||
let nsError = error as NSError
|
||||
print("💥 Error Saving WaypointEntity from WAYPOINT_APP \(nsError)")
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
print("💥 Error Deserializing WAYPOINT_APP packet.")
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,6 +3,6 @@
|
|||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>_XCCurrentVersionName</key>
|
||||
<string>MeshtasticDataModelV5.xcdatamodel</string>
|
||||
<string>MeshtasticDataModelV6.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="21513" systemVersion="22A400" minimumToolsVersion="Automatic" sourceLanguage="Swift" userDefinedModelVersionIdentifier="">
|
||||
<model type="com.apple.IDECoreDataModeler.DataModel" documentVersion="1.0" lastSavedToolsVersion="21513" systemVersion="22C65" minimumToolsVersion="Automatic" sourceLanguage="Swift" userDefinedModelVersionIdentifier="">
|
||||
<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"/>
|
||||
|
|
@ -269,6 +269,7 @@
|
|||
</entity>
|
||||
<entity name="WaypointEntity" representedClassName="WaypointEntity" syncable="YES" codeGenerationType="class">
|
||||
<attribute name="expire" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="icon" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="id" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="latitudeI" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="locked" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,294 @@
|
|||
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
||||
<model type="com.apple.IDECoreDataModeler.DataModel" documentVersion="1.0" lastSavedToolsVersion="21513" systemVersion="22D49" minimumToolsVersion="Automatic" sourceLanguage="Swift" userDefinedModelVersionIdentifier="">
|
||||
<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="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="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="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="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"/>
|
||||
<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"/>
|
||||
</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="regionCode" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="spreadFactor" optional="YES" attributeType="Integer 32" defaultValueString="0" 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="realACK" optional="YES" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
|
||||
<attribute name="receivedACK" attributeType="Boolean" usesScalarValueType="YES"/>
|
||||
<attribute name="replyID" optional="YES" attributeType="Integer 64" 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="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="bitrate" optional="YES" attributeType="Float" defaultValueString="0.0" usesScalarValueType="YES"/>
|
||||
<attribute name="bleName" optional="YES" attributeType="String"/>
|
||||
<attribute name="errorCount" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="firmwareVersion" attributeType="String"/>
|
||||
<attribute name="hasGps" attributeType="Boolean" usesScalarValueType="YES"/>
|
||||
<attribute name="hasWifi" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
|
||||
<attribute name="maxChannels" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="messageTimeoutMsec" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<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"/>
|
||||
<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="id" attributeType="Integer 64" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="lastHeard" attributeType="Date" usesScalarValueType="NO"/>
|
||||
<attribute name="num" attributeType="Integer 64" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="snr" optional="YES" attributeType="Float" defaultValueString="0.0" usesScalarValueType="YES"/>
|
||||
<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="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="serialConfig" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="SerialConfigEntity" inverseName="serialConfigNode" inverseEntity="SerialConfigEntity"/>
|
||||
<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="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="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="latitudeI" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="longitudeI" 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="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="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="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="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="longName" attributeType="String"/>
|
||||
<attribute name="macaddr" optional="YES" attributeType="Binary"/>
|
||||
<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"/>
|
||||
<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"/>
|
||||
</fetchedProperty>
|
||||
</entity>
|
||||
<entity name="WaypointEntity" representedClassName="WaypointEntity" syncable="YES" codeGenerationType="class">
|
||||
<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="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>
|
||||
|
|
@ -23,7 +23,7 @@ extension PositionEntity {
|
|||
return d / 1e7
|
||||
}
|
||||
|
||||
var coordinate: CLLocationCoordinate2D? {
|
||||
var nodeCoordinate: CLLocationCoordinate2D? {
|
||||
if latitudeI != 0 && longitudeI != 0 {
|
||||
let coord = CLLocationCoordinate2D(latitude: latitude!, longitude: longitude!)
|
||||
return coord
|
||||
|
|
@ -31,12 +31,18 @@ extension PositionEntity {
|
|||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
var annotaton: MKPointAnnotation {
|
||||
let pointAnn = MKPointAnnotation()
|
||||
if coordinate != nil {
|
||||
pointAnn.coordinate = coordinate!
|
||||
if nodeCoordinate != nil {
|
||||
pointAnn.coordinate = nodeCoordinate!
|
||||
}
|
||||
return pointAnn
|
||||
}
|
||||
}
|
||||
|
||||
extension PositionEntity: MKAnnotation {
|
||||
public var coordinate: CLLocationCoordinate2D { nodeCoordinate ?? LocationHelper.DefaultLocation }
|
||||
public var title: String? { nodePosition?.user?.shortName ?? NSLocalizedString("unknown", comment: "Unknown") }
|
||||
public var subtitle: String? { time?.formatted() }
|
||||
}
|
||||
|
|
|
|||
24
Meshtastic/Persistence/QueryCoreData.swift
Normal file
24
Meshtastic/Persistence/QueryCoreData.swift
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
//
|
||||
// QueryCoreData.swift
|
||||
// Meshtastic
|
||||
//
|
||||
// Created(c) Garth Vander Houwen 1/16/23.
|
||||
//
|
||||
|
||||
import CoreData
|
||||
|
||||
public func getWaypoint(id: Int64, context: NSManagedObjectContext) -> WaypointEntity {
|
||||
|
||||
let fetchWaypointRequest: NSFetchRequest<NSFetchRequestResult> = NSFetchRequest.init(entityName: "WaypointEntity")
|
||||
fetchWaypointRequest.predicate = NSPredicate(format: "id == %lld", Int64(id))
|
||||
|
||||
do {
|
||||
let fetchedWaypoint = try context.fetch(fetchWaypointRequest) as! [WaypointEntity]
|
||||
if fetchedWaypoint.count == 1 {
|
||||
return fetchedWaypoint[0]
|
||||
}
|
||||
} catch {
|
||||
return WaypointEntity(context: context)
|
||||
}
|
||||
return WaypointEntity(context: context)
|
||||
}
|
||||
|
|
@ -62,7 +62,7 @@ public func clearTelemetry(destNum: Int64, metricsType: Int32, context: NSManage
|
|||
|
||||
public func deleteChannelMessages(channel: ChannelEntity, context: NSManagedObjectContext) {
|
||||
do {
|
||||
let objects = channel.allPrivateMessages// try context.fetch(fetchChannelMessagesRequest) as! [NSManagedObject]
|
||||
let objects = channel.allPrivateMessages
|
||||
for object in objects {
|
||||
context.delete(object)
|
||||
}
|
||||
|
|
@ -75,7 +75,7 @@ public func deleteChannelMessages(channel: ChannelEntity, context: NSManagedObje
|
|||
public func deleteUserMessages(user: UserEntity, context: NSManagedObjectContext) {
|
||||
|
||||
do {
|
||||
let objects = user.messageList//try context.fetch(fetchUserMessagesRequest) as! [NSManagedObject]
|
||||
let objects = user.messageList
|
||||
for object in objects {
|
||||
context.delete(object)
|
||||
}
|
||||
|
|
@ -100,3 +100,354 @@ public func clearCoreDataDatabase(context: NSManagedObjectContext) {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
func upsertPositionPacket (packet: MeshPacket, context: NSManagedObjectContext) {
|
||||
|
||||
let logString = String.localizedStringWithFormat(NSLocalizedString("mesh.log.position.received %@", comment: "Position Packet received from node: %@"), String(packet.from))
|
||||
MeshLogger.log("📍 \(logString)")
|
||||
|
||||
let fetchNodePositionRequest: NSFetchRequest<NSFetchRequestResult> = NSFetchRequest.init(entityName: "NodeInfoEntity")
|
||||
fetchNodePositionRequest.predicate = NSPredicate(format: "num == %lld", Int64(packet.from))
|
||||
|
||||
do {
|
||||
|
||||
if let positionMessage = try? Position(serializedData: packet.decoded.payload) {
|
||||
|
||||
// Don't save empty position packets
|
||||
if positionMessage.longitudeI > 0 || positionMessage.latitudeI > 0 && (positionMessage.latitudeI != 373346000 && positionMessage.longitudeI != -1220090000)
|
||||
{
|
||||
let fetchedNode = try context.fetch(fetchNodePositionRequest) as! [NodeInfoEntity]
|
||||
if fetchedNode.count == 1 {
|
||||
|
||||
let position = PositionEntity(context: context)
|
||||
position.snr = packet.rxSnr
|
||||
position.seqNo = Int32(positionMessage.seqNumber)
|
||||
position.latitudeI = positionMessage.latitudeI
|
||||
position.longitudeI = positionMessage.longitudeI
|
||||
position.altitude = positionMessage.altitude
|
||||
position.satsInView = Int32(positionMessage.satsInView)
|
||||
position.speed = Int32(positionMessage.groundSpeed)
|
||||
position.heading = Int32(positionMessage.groundTrack)
|
||||
if positionMessage.timestamp != 0 {
|
||||
position.time = Date(timeIntervalSince1970: TimeInterval(Int64(positionMessage.timestamp)))
|
||||
} else {
|
||||
position.time = Date(timeIntervalSince1970: TimeInterval(Int64(positionMessage.time)))
|
||||
}
|
||||
let mutablePositions = fetchedNode[0].positions!.mutableCopy() as! NSMutableOrderedSet
|
||||
mutablePositions.add(position)
|
||||
fetchedNode[0].id = Int64(packet.from)
|
||||
fetchedNode[0].num = Int64(packet.from)
|
||||
fetchedNode[0].lastHeard = Date(timeIntervalSince1970: TimeInterval(Int64(positionMessage.time)))
|
||||
fetchedNode[0].snr = packet.rxSnr
|
||||
fetchedNode[0].positions = mutablePositions.copy() as? NSOrderedSet
|
||||
do {
|
||||
try context.save()
|
||||
print("💾 Updated Node Position Coordinates, SNR and Time from Position App Packet For: \(fetchedNode[0].num)")
|
||||
} catch {
|
||||
context.rollback()
|
||||
let nsError = error as NSError
|
||||
print("💥 Error Saving NodeInfoEntity from POSITION_APP \(nsError)")
|
||||
}
|
||||
}
|
||||
} else {
|
||||
print("💥 Empty POSITION_APP Packet")
|
||||
print(try! packet.jsonString())
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
print("💥 Error Deserializing POSITION_APP packet.")
|
||||
}
|
||||
}
|
||||
|
||||
func upsertBluetoothConfigPacket(config: Config, nodeNum: Int64, context: NSManagedObjectContext) {
|
||||
|
||||
let logString = String.localizedStringWithFormat(NSLocalizedString("mesh.log.bluetooth.config %@", comment: "Bluetooth config received: %@"), String(nodeNum))
|
||||
MeshLogger.log("📶 \(logString)")
|
||||
|
||||
let fetchNodeInfoRequest: NSFetchRequest<NSFetchRequestResult> = NSFetchRequest.init(entityName: "NodeInfoEntity")
|
||||
fetchNodeInfoRequest.predicate = NSPredicate(format: "num == %lld", Int64(nodeNum))
|
||||
|
||||
do {
|
||||
|
||||
let fetchedNode = try context.fetch(fetchNodeInfoRequest) as! [NodeInfoEntity]
|
||||
// Found a node, save Device Config
|
||||
if !fetchedNode.isEmpty {
|
||||
if fetchedNode[0].bluetoothConfig == nil {
|
||||
let newBluetoothConfig = BluetoothConfigEntity(context: context)
|
||||
newBluetoothConfig.enabled = config.bluetooth.enabled
|
||||
newBluetoothConfig.mode = Int32(config.bluetooth.mode.rawValue)
|
||||
newBluetoothConfig.fixedPin = Int32(config.bluetooth.fixedPin)
|
||||
fetchedNode[0].bluetoothConfig = newBluetoothConfig
|
||||
} else {
|
||||
fetchedNode[0].bluetoothConfig?.enabled = config.bluetooth.enabled
|
||||
fetchedNode[0].bluetoothConfig?.mode = Int32(config.bluetooth.mode.rawValue)
|
||||
fetchedNode[0].bluetoothConfig?.fixedPin = Int32(config.bluetooth.fixedPin)
|
||||
}
|
||||
do {
|
||||
try context.save()
|
||||
print("💾 Updated Bluetooth Config for node number: \(String(nodeNum))")
|
||||
} catch {
|
||||
context.rollback()
|
||||
let nsError = error as NSError
|
||||
print("💥 Error Updating Core Data BluetoothConfigEntity: \(nsError)")
|
||||
}
|
||||
} else {
|
||||
print("💥 No Nodes found in local database matching node number \(nodeNum) unable to save Bluetooth Config")
|
||||
}
|
||||
} catch {
|
||||
let nsError = error as NSError
|
||||
print("💥 Fetching node for core data BluetoothConfigEntity failed: \(nsError)")
|
||||
}
|
||||
}
|
||||
|
||||
func upsertDeviceConfigPacket(config: Config, nodeNum: Int64, context: NSManagedObjectContext) {
|
||||
|
||||
let logString = String.localizedStringWithFormat(NSLocalizedString("mesh.log.device.config %@", comment: "Device config received: %@"), String(nodeNum))
|
||||
MeshLogger.log("📟 \(logString)")
|
||||
let fetchNodeInfoRequest: NSFetchRequest<NSFetchRequestResult> = NSFetchRequest.init(entityName: "NodeInfoEntity")
|
||||
fetchNodeInfoRequest.predicate = NSPredicate(format: "num == %lld", Int64(nodeNum))
|
||||
|
||||
do {
|
||||
|
||||
let fetchedNode = try context.fetch(fetchNodeInfoRequest) as! [NodeInfoEntity]
|
||||
// Found a node, save Device Config
|
||||
if !fetchedNode.isEmpty {
|
||||
if fetchedNode[0].deviceConfig == nil {
|
||||
let newDeviceConfig = DeviceConfigEntity(context: context)
|
||||
newDeviceConfig.role = Int32(config.device.role.rawValue)
|
||||
newDeviceConfig.serialEnabled = config.device.serialEnabled
|
||||
newDeviceConfig.debugLogEnabled = config.device.debugLogEnabled
|
||||
newDeviceConfig.buttonGpio = Int32(config.device.buttonGpio)
|
||||
newDeviceConfig.buzzerGpio = Int32(config.device.buzzerGpio)
|
||||
fetchedNode[0].deviceConfig = newDeviceConfig
|
||||
} else {
|
||||
fetchedNode[0].deviceConfig?.role = Int32(config.device.role.rawValue)
|
||||
fetchedNode[0].deviceConfig?.serialEnabled = config.device.serialEnabled
|
||||
fetchedNode[0].deviceConfig?.debugLogEnabled = config.device.debugLogEnabled
|
||||
fetchedNode[0].deviceConfig?.buttonGpio = Int32(config.device.buttonGpio)
|
||||
fetchedNode[0].deviceConfig?.buzzerGpio = Int32(config.device.buzzerGpio)
|
||||
}
|
||||
do {
|
||||
try context.save()
|
||||
print("💾 Updated Device Config for node number: \(String(nodeNum))")
|
||||
} catch {
|
||||
context.rollback()
|
||||
let nsError = error as NSError
|
||||
print("💥 Error Updating Core Data DeviceConfigEntity: \(nsError)")
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
let nsError = error as NSError
|
||||
print("💥 Fetching node for core data DeviceConfigEntity failed: \(nsError)")
|
||||
}
|
||||
}
|
||||
|
||||
func upsertDisplayConfigPacket(config: Config, nodeNum: Int64, context: NSManagedObjectContext) {
|
||||
|
||||
let logString = String.localizedStringWithFormat(NSLocalizedString("mesh.log.display.config %@", comment: "Display config received: %@"), String(nodeNum))
|
||||
MeshLogger.log("🖥️ \(logString)")
|
||||
|
||||
let fetchNodeInfoRequest: NSFetchRequest<NSFetchRequestResult> = NSFetchRequest.init(entityName: "NodeInfoEntity")
|
||||
fetchNodeInfoRequest.predicate = NSPredicate(format: "num == %lld", Int64(nodeNum))
|
||||
|
||||
do {
|
||||
|
||||
let fetchedNode = try context.fetch(fetchNodeInfoRequest) as! [NodeInfoEntity]
|
||||
|
||||
// Found a node, save Device Config
|
||||
if !fetchedNode.isEmpty {
|
||||
|
||||
if fetchedNode[0].displayConfig == nil {
|
||||
|
||||
let newDisplayConfig = DisplayConfigEntity(context: context)
|
||||
newDisplayConfig.gpsFormat = Int32(config.display.gpsFormat.rawValue)
|
||||
newDisplayConfig.screenOnSeconds = Int32(config.display.screenOnSecs)
|
||||
newDisplayConfig.screenCarouselInterval = Int32(config.display.autoScreenCarouselSecs)
|
||||
newDisplayConfig.compassNorthTop = config.display.compassNorthTop
|
||||
newDisplayConfig.flipScreen = config.display.flipScreen
|
||||
newDisplayConfig.oledType = Int32(config.display.oled.rawValue)
|
||||
newDisplayConfig.displayMode = Int32(config.display.displaymode.rawValue)
|
||||
fetchedNode[0].displayConfig = newDisplayConfig
|
||||
|
||||
} else {
|
||||
|
||||
fetchedNode[0].displayConfig?.gpsFormat = Int32(config.display.gpsFormat.rawValue)
|
||||
fetchedNode[0].displayConfig?.screenOnSeconds = Int32(config.display.screenOnSecs)
|
||||
fetchedNode[0].displayConfig?.screenCarouselInterval = Int32(config.display.autoScreenCarouselSecs)
|
||||
fetchedNode[0].displayConfig?.compassNorthTop = config.display.compassNorthTop
|
||||
fetchedNode[0].displayConfig?.flipScreen = config.display.flipScreen
|
||||
fetchedNode[0].displayConfig?.oledType = Int32(config.display.oled.rawValue)
|
||||
fetchedNode[0].displayConfig?.displayMode = Int32(config.display.displaymode.rawValue)
|
||||
}
|
||||
|
||||
do {
|
||||
|
||||
try context.save()
|
||||
print("💾 Updated Display Config for node number: \(String(nodeNum))")
|
||||
|
||||
} catch {
|
||||
|
||||
context.rollback()
|
||||
|
||||
let nsError = error as NSError
|
||||
print("💥 Error Updating Core Data DisplayConfigEntity: \(nsError)")
|
||||
}
|
||||
} else {
|
||||
|
||||
print("💥 No Nodes found in local database matching node number \(nodeNum) unable to save Display Config")
|
||||
}
|
||||
|
||||
} catch {
|
||||
|
||||
let nsError = error as NSError
|
||||
print("💥 Fetching node for core data DisplayConfigEntity failed: \(nsError)")
|
||||
}
|
||||
}
|
||||
|
||||
func upsertLoRaConfigPacket(config: Config, nodeNum: Int64, context: NSManagedObjectContext) {
|
||||
|
||||
let logString = String.localizedStringWithFormat(NSLocalizedString("mesh.log.lora.config %@", comment: "LoRa config received: %@"), String(nodeNum))
|
||||
MeshLogger.log("📻 \(logString)")
|
||||
|
||||
let fetchNodeInfoRequest: NSFetchRequest<NSFetchRequestResult> = NSFetchRequest.init(entityName: "NodeInfoEntity")
|
||||
fetchNodeInfoRequest.predicate = NSPredicate(format: "num == %lld", nodeNum)
|
||||
|
||||
do {
|
||||
|
||||
let fetchedNode = try context.fetch(fetchNodeInfoRequest) as! [NodeInfoEntity]
|
||||
// Found a node, save LoRa Config
|
||||
if fetchedNode.count > 0 {
|
||||
if fetchedNode[0].loRaConfig == nil {
|
||||
// No lora config for node, save a new lora config
|
||||
let newLoRaConfig = LoRaConfigEntity(context: context)
|
||||
newLoRaConfig.regionCode = Int32(config.lora.region.rawValue)
|
||||
newLoRaConfig.usePreset = config.lora.usePreset
|
||||
newLoRaConfig.modemPreset = Int32(config.lora.modemPreset.rawValue)
|
||||
newLoRaConfig.bandwidth = Int32(config.lora.bandwidth)
|
||||
newLoRaConfig.spreadFactor = Int32(config.lora.spreadFactor)
|
||||
newLoRaConfig.codingRate = Int32(config.lora.codingRate)
|
||||
newLoRaConfig.frequencyOffset = config.lora.frequencyOffset
|
||||
newLoRaConfig.hopLimit = Int32(config.lora.hopLimit)
|
||||
newLoRaConfig.txPower = Int32(config.lora.txPower)
|
||||
newLoRaConfig.txEnabled = config.lora.txEnabled
|
||||
newLoRaConfig.channelNum = Int32(config.lora.channelNum)
|
||||
fetchedNode[0].loRaConfig = newLoRaConfig
|
||||
} else {
|
||||
fetchedNode[0].loRaConfig?.regionCode = Int32(config.lora.region.rawValue)
|
||||
fetchedNode[0].loRaConfig?.usePreset = config.lora.usePreset
|
||||
fetchedNode[0].loRaConfig?.modemPreset = Int32(config.lora.modemPreset.rawValue)
|
||||
fetchedNode[0].loRaConfig?.bandwidth = Int32(config.lora.bandwidth)
|
||||
fetchedNode[0].loRaConfig?.spreadFactor = Int32(config.lora.spreadFactor)
|
||||
fetchedNode[0].loRaConfig?.codingRate = Int32(config.lora.codingRate)
|
||||
fetchedNode[0].loRaConfig?.frequencyOffset = config.lora.frequencyOffset
|
||||
fetchedNode[0].loRaConfig?.hopLimit = Int32(config.lora.hopLimit)
|
||||
fetchedNode[0].loRaConfig?.txPower = Int32(config.lora.txPower)
|
||||
fetchedNode[0].loRaConfig?.txEnabled = config.lora.txEnabled
|
||||
fetchedNode[0].loRaConfig?.channelNum = Int32(config.lora.channelNum)
|
||||
}
|
||||
do {
|
||||
try context.save()
|
||||
print("💾 Updated LoRa Config for node number: \(String(nodeNum))")
|
||||
} catch {
|
||||
context.rollback()
|
||||
let nsError = error as NSError
|
||||
print("💥 Error Updating Core Data LoRaConfigEntity: \(nsError)")
|
||||
}
|
||||
} else {
|
||||
print("💥 No Nodes found in local database matching node number \(nodeNum) unable to save Lora Config")
|
||||
}
|
||||
} catch {
|
||||
let nsError = error as NSError
|
||||
print("💥 Fetching node for core data LoRaConfigEntity failed: \(nsError)")
|
||||
}
|
||||
}
|
||||
|
||||
func upsertNetworkConfigPacket(config: Config, nodeNum: Int64, context: NSManagedObjectContext) {
|
||||
|
||||
let logString = String.localizedStringWithFormat(NSLocalizedString("mesh.log.network.config %@", comment: "Network config received: %@"), String(nodeNum))
|
||||
MeshLogger.log("🌐 \(logString)")
|
||||
|
||||
let fetchNodeInfoRequest: NSFetchRequest<NSFetchRequestResult> = NSFetchRequest.init(entityName: "NodeInfoEntity")
|
||||
fetchNodeInfoRequest.predicate = NSPredicate(format: "num == %lld", Int64(nodeNum))
|
||||
|
||||
do {
|
||||
|
||||
let fetchedNode = try context.fetch(fetchNodeInfoRequest) as! [NodeInfoEntity]
|
||||
// Found a node, save WiFi Config
|
||||
if !fetchedNode.isEmpty {
|
||||
if fetchedNode[0].networkConfig == nil {
|
||||
let newNetworkConfig = NetworkConfigEntity(context: context)
|
||||
newNetworkConfig.wifiSsid = config.network.wifiSsid
|
||||
newNetworkConfig.wifiPsk = config.network.wifiPsk
|
||||
fetchedNode[0].networkConfig = newNetworkConfig
|
||||
} else {
|
||||
fetchedNode[0].networkConfig?.wifiSsid = config.network.wifiSsid
|
||||
fetchedNode[0].networkConfig?.wifiPsk = config.network.wifiPsk
|
||||
}
|
||||
|
||||
do {
|
||||
try context.save()
|
||||
print("💾 Updated Network Config for node number: \(String(nodeNum))")
|
||||
|
||||
} catch {
|
||||
context.rollback()
|
||||
let nsError = error as NSError
|
||||
print("💥 Error Updating Core Data WiFiConfigEntity: \(nsError)")
|
||||
}
|
||||
} else {
|
||||
print("💥 No Nodes found in local database matching node number \(nodeNum) unable to save Network Config")
|
||||
}
|
||||
} catch {
|
||||
let nsError = error as NSError
|
||||
print("💥 Fetching node for core data NetworkConfigEntity failed: \(nsError)")
|
||||
}
|
||||
}
|
||||
|
||||
func upsertPositionConfigPacket(config: Config, nodeNum: Int64, context: NSManagedObjectContext) {
|
||||
|
||||
let logString = String.localizedStringWithFormat(NSLocalizedString("mesh.log.position.config %@", comment: "Positon config received: %@"), String(nodeNum))
|
||||
MeshLogger.log("🗺️ \(logString)")
|
||||
|
||||
let fetchNodeInfoRequest: NSFetchRequest<NSFetchRequestResult> = NSFetchRequest.init(entityName: "NodeInfoEntity")
|
||||
fetchNodeInfoRequest.predicate = NSPredicate(format: "num == %lld", Int64(nodeNum))
|
||||
|
||||
do {
|
||||
|
||||
let fetchedNode = try context.fetch(fetchNodeInfoRequest) as! [NodeInfoEntity]
|
||||
// Found a node, save LoRa Config
|
||||
if !fetchedNode.isEmpty {
|
||||
if fetchedNode[0].positionConfig == nil {
|
||||
let newPositionConfig = PositionConfigEntity(context: context)
|
||||
newPositionConfig.smartPositionEnabled = config.position.positionBroadcastSmartEnabled
|
||||
newPositionConfig.deviceGpsEnabled = config.position.gpsEnabled
|
||||
newPositionConfig.fixedPosition = config.position.fixedPosition
|
||||
newPositionConfig.gpsUpdateInterval = Int32(config.position.gpsUpdateInterval)
|
||||
newPositionConfig.gpsAttemptTime = Int32(config.position.gpsAttemptTime)
|
||||
newPositionConfig.positionBroadcastSeconds = Int32(config.position.positionBroadcastSecs)
|
||||
newPositionConfig.positionFlags = Int32(config.position.positionFlags)
|
||||
fetchedNode[0].positionConfig = newPositionConfig
|
||||
} else {
|
||||
fetchedNode[0].positionConfig?.smartPositionEnabled = config.position.positionBroadcastSmartEnabled
|
||||
fetchedNode[0].positionConfig?.deviceGpsEnabled = config.position.gpsEnabled
|
||||
fetchedNode[0].positionConfig?.fixedPosition = config.position.fixedPosition
|
||||
fetchedNode[0].positionConfig?.gpsUpdateInterval = Int32(config.position.gpsUpdateInterval)
|
||||
fetchedNode[0].positionConfig?.gpsAttemptTime = Int32(config.position.gpsAttemptTime)
|
||||
fetchedNode[0].positionConfig?.positionBroadcastSeconds = Int32(config.position.positionBroadcastSecs)
|
||||
fetchedNode[0].positionConfig?.positionFlags = Int32(config.position.positionFlags)
|
||||
}
|
||||
do {
|
||||
try context.save()
|
||||
print("💾 Updated Position Config for node number: \(String(nodeNum))")
|
||||
} catch {
|
||||
context.rollback()
|
||||
let nsError = error as NSError
|
||||
print("💥 Error Updating Core Data PositionConfigEntity: \(nsError)")
|
||||
}
|
||||
} else {
|
||||
print("💥 No Nodes found in local database matching node number \(nodeNum) unable to save Position Config")
|
||||
}
|
||||
} catch {
|
||||
let nsError = error as NSError
|
||||
print("💥 Fetching node for core data PositionConfigEntity failed: \(nsError)")
|
||||
}
|
||||
}
|
||||
|
|
|
|||
57
Meshtastic/Persistence/WaypointEntityExtension.swift
Normal file
57
Meshtastic/Persistence/WaypointEntityExtension.swift
Normal file
|
|
@ -0,0 +1,57 @@
|
|||
//
|
||||
// WaypointEntityExtension.swift
|
||||
// Meshtastic
|
||||
//
|
||||
// Copyright (c) Garth Vander Houwen 1/13/23.
|
||||
//
|
||||
import CoreData
|
||||
import CoreLocation
|
||||
import MapKit
|
||||
import SwiftUI
|
||||
|
||||
extension WaypointEntity {
|
||||
|
||||
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 waypointCoordinate: CLLocationCoordinate2D? {
|
||||
if latitudeI != 0 && longitudeI != 0 {
|
||||
let coord = CLLocationCoordinate2D(latitude: latitude!, longitude: longitude!)
|
||||
return coord
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
var annotaton: MKPointAnnotation {
|
||||
let pointAnn = MKPointAnnotation()
|
||||
if waypointCoordinate != nil {
|
||||
pointAnn.coordinate = waypointCoordinate!
|
||||
}
|
||||
return pointAnn
|
||||
}
|
||||
}
|
||||
|
||||
extension WaypointEntity: MKAnnotation {
|
||||
public var coordinate: CLLocationCoordinate2D { waypointCoordinate ?? LocationHelper.DefaultLocation }
|
||||
public var title: String? { name ?? "Dropped Pin" }
|
||||
public var subtitle: String? {
|
||||
(longDescription ?? "") +
|
||||
String(expire != nil ? "\n⌛ Expires \(String(describing: expire?.formatted()))" : "") +
|
||||
String(locked > 0 ? "\n🔒 Locked" : "") }
|
||||
}
|
||||
|
|
@ -487,6 +487,10 @@ struct Config {
|
|||
/// Clears the value of `ipv4Config`. Subsequent reads from it will return its default value.
|
||||
mutating func clearIpv4Config() {self._ipv4Config = nil}
|
||||
|
||||
///
|
||||
/// rsyslog Server and Port
|
||||
var rsyslogServer: String = String()
|
||||
|
||||
var unknownFields = SwiftProtobuf.UnknownStorage()
|
||||
|
||||
enum AddressMode: SwiftProtobuf.Enum {
|
||||
|
|
@ -941,6 +945,14 @@ struct Config {
|
|||
///
|
||||
/// WLAN Band
|
||||
case lora24 // = 13
|
||||
|
||||
///
|
||||
/// Ukraine 433mhz
|
||||
case ua433 // = 14
|
||||
|
||||
///
|
||||
/// Ukraine 868mhz
|
||||
case ua868 // = 15
|
||||
case UNRECOGNIZED(Int)
|
||||
|
||||
init() {
|
||||
|
|
@ -963,6 +975,8 @@ struct Config {
|
|||
case 11: self = .nz865
|
||||
case 12: self = .th
|
||||
case 13: self = .lora24
|
||||
case 14: self = .ua433
|
||||
case 15: self = .ua868
|
||||
default: self = .UNRECOGNIZED(rawValue)
|
||||
}
|
||||
}
|
||||
|
|
@ -983,6 +997,8 @@ struct Config {
|
|||
case .nz865: return 11
|
||||
case .th: return 12
|
||||
case .lora24: return 13
|
||||
case .ua433: return 14
|
||||
case .ua868: return 15
|
||||
case .UNRECOGNIZED(let i): return i
|
||||
}
|
||||
}
|
||||
|
|
@ -1217,6 +1233,8 @@ extension Config.LoRaConfig.RegionCode: CaseIterable {
|
|||
.nz865,
|
||||
.th,
|
||||
.lora24,
|
||||
.ua433,
|
||||
.ua868,
|
||||
]
|
||||
}
|
||||
|
||||
|
|
@ -1674,6 +1692,7 @@ extension Config.NetworkConfig: SwiftProtobuf.Message, SwiftProtobuf._MessageImp
|
|||
6: .standard(proto: "eth_enabled"),
|
||||
7: .standard(proto: "address_mode"),
|
||||
8: .standard(proto: "ipv4_config"),
|
||||
9: .standard(proto: "rsyslog_server"),
|
||||
]
|
||||
|
||||
mutating func decodeMessage<D: SwiftProtobuf.Decoder>(decoder: inout D) throws {
|
||||
|
|
@ -1689,6 +1708,7 @@ extension Config.NetworkConfig: SwiftProtobuf.Message, SwiftProtobuf._MessageImp
|
|||
case 6: try { try decoder.decodeSingularBoolField(value: &self.ethEnabled) }()
|
||||
case 7: try { try decoder.decodeSingularEnumField(value: &self.addressMode) }()
|
||||
case 8: try { try decoder.decodeSingularMessageField(value: &self._ipv4Config) }()
|
||||
case 9: try { try decoder.decodeSingularStringField(value: &self.rsyslogServer) }()
|
||||
default: break
|
||||
}
|
||||
}
|
||||
|
|
@ -1720,6 +1740,9 @@ extension Config.NetworkConfig: SwiftProtobuf.Message, SwiftProtobuf._MessageImp
|
|||
try { if let v = self._ipv4Config {
|
||||
try visitor.visitSingularMessageField(value: v, fieldNumber: 8)
|
||||
} }()
|
||||
if !self.rsyslogServer.isEmpty {
|
||||
try visitor.visitSingularStringField(value: self.rsyslogServer, fieldNumber: 9)
|
||||
}
|
||||
try unknownFields.traverse(visitor: &visitor)
|
||||
}
|
||||
|
||||
|
|
@ -1731,6 +1754,7 @@ extension Config.NetworkConfig: SwiftProtobuf.Message, SwiftProtobuf._MessageImp
|
|||
if lhs.ethEnabled != rhs.ethEnabled {return false}
|
||||
if lhs.addressMode != rhs.addressMode {return false}
|
||||
if lhs._ipv4Config != rhs._ipv4Config {return false}
|
||||
if lhs.rsyslogServer != rhs.rsyslogServer {return false}
|
||||
if lhs.unknownFields != rhs.unknownFields {return false}
|
||||
return true
|
||||
}
|
||||
|
|
@ -2028,6 +2052,8 @@ extension Config.LoRaConfig.RegionCode: SwiftProtobuf._ProtoNameProviding {
|
|||
11: .same(proto: "NZ_865"),
|
||||
12: .same(proto: "TH"),
|
||||
13: .same(proto: "LORA_24"),
|
||||
14: .same(proto: "UA_433"),
|
||||
15: .same(proto: "UA_868"),
|
||||
]
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -51,6 +51,14 @@ struct DeviceMetadata {
|
|||
/// Indicates that the device has an ethernet peripheral
|
||||
var hasEthernet_p: Bool = false
|
||||
|
||||
///
|
||||
/// Indicates that the device's role in the mesh
|
||||
var role: Config.DeviceConfig.Role = .client
|
||||
|
||||
///
|
||||
/// Indicates the device's current enabled position flags
|
||||
var positionFlags: UInt32 = 0
|
||||
|
||||
var unknownFields = SwiftProtobuf.UnknownStorage()
|
||||
|
||||
init() {}
|
||||
|
|
@ -71,6 +79,8 @@ extension DeviceMetadata: SwiftProtobuf.Message, SwiftProtobuf._MessageImplement
|
|||
4: .same(proto: "hasWifi"),
|
||||
5: .same(proto: "hasBluetooth"),
|
||||
6: .same(proto: "hasEthernet"),
|
||||
7: .same(proto: "role"),
|
||||
8: .standard(proto: "position_flags"),
|
||||
]
|
||||
|
||||
mutating func decodeMessage<D: SwiftProtobuf.Decoder>(decoder: inout D) throws {
|
||||
|
|
@ -85,6 +95,8 @@ extension DeviceMetadata: SwiftProtobuf.Message, SwiftProtobuf._MessageImplement
|
|||
case 4: try { try decoder.decodeSingularBoolField(value: &self.hasWifi_p) }()
|
||||
case 5: try { try decoder.decodeSingularBoolField(value: &self.hasBluetooth_p) }()
|
||||
case 6: try { try decoder.decodeSingularBoolField(value: &self.hasEthernet_p) }()
|
||||
case 7: try { try decoder.decodeSingularEnumField(value: &self.role) }()
|
||||
case 8: try { try decoder.decodeSingularUInt32Field(value: &self.positionFlags) }()
|
||||
default: break
|
||||
}
|
||||
}
|
||||
|
|
@ -109,6 +121,12 @@ extension DeviceMetadata: SwiftProtobuf.Message, SwiftProtobuf._MessageImplement
|
|||
if self.hasEthernet_p != false {
|
||||
try visitor.visitSingularBoolField(value: self.hasEthernet_p, fieldNumber: 6)
|
||||
}
|
||||
if self.role != .client {
|
||||
try visitor.visitSingularEnumField(value: self.role, fieldNumber: 7)
|
||||
}
|
||||
if self.positionFlags != 0 {
|
||||
try visitor.visitSingularUInt32Field(value: self.positionFlags, fieldNumber: 8)
|
||||
}
|
||||
try unknownFields.traverse(visitor: &visitor)
|
||||
}
|
||||
|
||||
|
|
@ -119,6 +137,8 @@ extension DeviceMetadata: SwiftProtobuf.Message, SwiftProtobuf._MessageImplement
|
|||
if lhs.hasWifi_p != rhs.hasWifi_p {return false}
|
||||
if lhs.hasBluetooth_p != rhs.hasBluetooth_p {return false}
|
||||
if lhs.hasEthernet_p != rhs.hasEthernet_p {return false}
|
||||
if lhs.role != rhs.role {return false}
|
||||
if lhs.positionFlags != rhs.positionFlags {return false}
|
||||
if lhs.unknownFields != rhs.unknownFields {return false}
|
||||
return true
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1143,17 +1143,22 @@ struct Waypoint {
|
|||
var expire: UInt32 = 0
|
||||
|
||||
///
|
||||
/// If true, only allow the original sender to update the waypoint.
|
||||
var locked: Bool = false
|
||||
/// If greater than zero, treat the value as a nodenum only allowing them to update the waypoint.
|
||||
/// If zero, the waypoint is open to be edited by any member of the mesh.
|
||||
var lockedTo: UInt32 = 0
|
||||
|
||||
///
|
||||
/// Name of the waypoint - max 30 chars
|
||||
var name: String = String()
|
||||
|
||||
///*
|
||||
///
|
||||
/// Description of the waypoint - max 100 chars
|
||||
var description_p: String = String()
|
||||
|
||||
///
|
||||
/// Designator icon for the waypoint in the form of a unicode emoji
|
||||
var icon: UInt32 = 0
|
||||
|
||||
var unknownFields = SwiftProtobuf.UnknownStorage()
|
||||
|
||||
init() {}
|
||||
|
|
@ -1924,7 +1929,8 @@ struct FromRadio {
|
|||
set {_uniqueStorage()._payloadVariant = .channel(newValue)}
|
||||
}
|
||||
|
||||
/// Queue status info
|
||||
///
|
||||
/// Queue status info
|
||||
var queueStatus: QueueStatus {
|
||||
get {
|
||||
if case .queueStatus(let v)? = _storage._payloadVariant {return v}
|
||||
|
|
@ -1933,6 +1939,16 @@ struct FromRadio {
|
|||
set {_uniqueStorage()._payloadVariant = .queueStatus(newValue)}
|
||||
}
|
||||
|
||||
///
|
||||
/// File Transfer Chunk
|
||||
var xmodemPacket: XModem {
|
||||
get {
|
||||
if case .xmodemPacket(let v)? = _storage._payloadVariant {return v}
|
||||
return XModem()
|
||||
}
|
||||
set {_uniqueStorage()._payloadVariant = .xmodemPacket(newValue)}
|
||||
}
|
||||
|
||||
var unknownFields = SwiftProtobuf.UnknownStorage()
|
||||
|
||||
///
|
||||
|
|
@ -1973,8 +1989,12 @@ struct FromRadio {
|
|||
///
|
||||
/// One packet is sent for each channel
|
||||
case channel(Channel)
|
||||
/// Queue status info
|
||||
///
|
||||
/// Queue status info
|
||||
case queueStatus(QueueStatus)
|
||||
///
|
||||
/// File Transfer Chunk
|
||||
case xmodemPacket(XModem)
|
||||
|
||||
#if !swift(>=4.1)
|
||||
static func ==(lhs: FromRadio.OneOf_PayloadVariant, rhs: FromRadio.OneOf_PayloadVariant) -> Bool {
|
||||
|
|
@ -2022,6 +2042,10 @@ struct FromRadio {
|
|||
guard case .queueStatus(let l) = lhs, case .queueStatus(let r) = rhs else { preconditionFailure() }
|
||||
return l == r
|
||||
}()
|
||||
case (.xmodemPacket, .xmodemPacket): return {
|
||||
guard case .xmodemPacket(let l) = lhs, case .xmodemPacket(let r) = rhs else { preconditionFailure() }
|
||||
return l == r
|
||||
}()
|
||||
default: return false
|
||||
}
|
||||
}
|
||||
|
|
@ -2084,6 +2108,14 @@ struct ToRadio {
|
|||
set {payloadVariant = .disconnect(newValue)}
|
||||
}
|
||||
|
||||
var xmodemPacket: XModem {
|
||||
get {
|
||||
if case .xmodemPacket(let v)? = payloadVariant {return v}
|
||||
return XModem()
|
||||
}
|
||||
set {payloadVariant = .xmodemPacket(newValue)}
|
||||
}
|
||||
|
||||
var unknownFields = SwiftProtobuf.UnknownStorage()
|
||||
|
||||
///
|
||||
|
|
@ -2107,6 +2139,7 @@ struct ToRadio {
|
|||
/// This is useful for serial links where there is no hardware/protocol based notification that the client has dropped the link.
|
||||
/// (Sending this message is optional for clients)
|
||||
case disconnect(Bool)
|
||||
case xmodemPacket(XModem)
|
||||
|
||||
#if !swift(>=4.1)
|
||||
static func ==(lhs: ToRadio.OneOf_PayloadVariant, rhs: ToRadio.OneOf_PayloadVariant) -> Bool {
|
||||
|
|
@ -2126,6 +2159,10 @@ struct ToRadio {
|
|||
guard case .disconnect(let l) = lhs, case .disconnect(let r) = rhs else { preconditionFailure() }
|
||||
return l == r
|
||||
}()
|
||||
case (.xmodemPacket, .xmodemPacket): return {
|
||||
guard case .xmodemPacket(let l) = lhs, case .xmodemPacket(let r) = rhs else { preconditionFailure() }
|
||||
return l == r
|
||||
}()
|
||||
default: return false
|
||||
}
|
||||
}
|
||||
|
|
@ -2775,9 +2812,10 @@ extension Waypoint: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationB
|
|||
2: .standard(proto: "latitude_i"),
|
||||
3: .standard(proto: "longitude_i"),
|
||||
4: .same(proto: "expire"),
|
||||
5: .same(proto: "locked"),
|
||||
5: .standard(proto: "locked_to"),
|
||||
6: .same(proto: "name"),
|
||||
7: .same(proto: "description"),
|
||||
8: .same(proto: "icon"),
|
||||
]
|
||||
|
||||
mutating func decodeMessage<D: SwiftProtobuf.Decoder>(decoder: inout D) throws {
|
||||
|
|
@ -2790,9 +2828,10 @@ extension Waypoint: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationB
|
|||
case 2: try { try decoder.decodeSingularSFixed32Field(value: &self.latitudeI) }()
|
||||
case 3: try { try decoder.decodeSingularSFixed32Field(value: &self.longitudeI) }()
|
||||
case 4: try { try decoder.decodeSingularUInt32Field(value: &self.expire) }()
|
||||
case 5: try { try decoder.decodeSingularBoolField(value: &self.locked) }()
|
||||
case 5: try { try decoder.decodeSingularUInt32Field(value: &self.lockedTo) }()
|
||||
case 6: try { try decoder.decodeSingularStringField(value: &self.name) }()
|
||||
case 7: try { try decoder.decodeSingularStringField(value: &self.description_p) }()
|
||||
case 8: try { try decoder.decodeSingularFixed32Field(value: &self.icon) }()
|
||||
default: break
|
||||
}
|
||||
}
|
||||
|
|
@ -2811,8 +2850,8 @@ extension Waypoint: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationB
|
|||
if self.expire != 0 {
|
||||
try visitor.visitSingularUInt32Field(value: self.expire, fieldNumber: 4)
|
||||
}
|
||||
if self.locked != false {
|
||||
try visitor.visitSingularBoolField(value: self.locked, fieldNumber: 5)
|
||||
if self.lockedTo != 0 {
|
||||
try visitor.visitSingularUInt32Field(value: self.lockedTo, fieldNumber: 5)
|
||||
}
|
||||
if !self.name.isEmpty {
|
||||
try visitor.visitSingularStringField(value: self.name, fieldNumber: 6)
|
||||
|
|
@ -2820,6 +2859,9 @@ extension Waypoint: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationB
|
|||
if !self.description_p.isEmpty {
|
||||
try visitor.visitSingularStringField(value: self.description_p, fieldNumber: 7)
|
||||
}
|
||||
if self.icon != 0 {
|
||||
try visitor.visitSingularFixed32Field(value: self.icon, fieldNumber: 8)
|
||||
}
|
||||
try unknownFields.traverse(visitor: &visitor)
|
||||
}
|
||||
|
||||
|
|
@ -2828,9 +2870,10 @@ extension Waypoint: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationB
|
|||
if lhs.latitudeI != rhs.latitudeI {return false}
|
||||
if lhs.longitudeI != rhs.longitudeI {return false}
|
||||
if lhs.expire != rhs.expire {return false}
|
||||
if lhs.locked != rhs.locked {return false}
|
||||
if lhs.lockedTo != rhs.lockedTo {return false}
|
||||
if lhs.name != rhs.name {return false}
|
||||
if lhs.description_p != rhs.description_p {return false}
|
||||
if lhs.icon != rhs.icon {return false}
|
||||
if lhs.unknownFields != rhs.unknownFields {return false}
|
||||
return true
|
||||
}
|
||||
|
|
@ -3355,6 +3398,7 @@ extension FromRadio: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementation
|
|||
9: .same(proto: "moduleConfig"),
|
||||
10: .same(proto: "channel"),
|
||||
11: .same(proto: "queueStatus"),
|
||||
12: .same(proto: "xmodemPacket"),
|
||||
]
|
||||
|
||||
fileprivate class _StorageClass {
|
||||
|
|
@ -3507,6 +3551,19 @@ extension FromRadio: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementation
|
|||
_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)
|
||||
}
|
||||
}()
|
||||
default: break
|
||||
}
|
||||
}
|
||||
|
|
@ -3563,6 +3620,10 @@ extension FromRadio: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementation
|
|||
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 nil: break
|
||||
}
|
||||
}
|
||||
|
|
@ -3591,6 +3652,7 @@ extension ToRadio: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBa
|
|||
1: .same(proto: "packet"),
|
||||
3: .standard(proto: "want_config_id"),
|
||||
4: .same(proto: "disconnect"),
|
||||
5: .same(proto: "xmodemPacket"),
|
||||
]
|
||||
|
||||
mutating func decodeMessage<D: SwiftProtobuf.Decoder>(decoder: inout D) throws {
|
||||
|
|
@ -3628,6 +3690,19 @@ extension ToRadio: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBa
|
|||
self.payloadVariant = .disconnect(v)
|
||||
}
|
||||
}()
|
||||
case 5: 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)
|
||||
}
|
||||
}()
|
||||
default: break
|
||||
}
|
||||
}
|
||||
|
|
@ -3651,6 +3726,10 @@ extension ToRadio: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBa
|
|||
guard case .disconnect(let v)? = self.payloadVariant else { preconditionFailure() }
|
||||
try visitor.visitSingularBoolField(value: v, fieldNumber: 4)
|
||||
}()
|
||||
case .xmodemPacket?: try {
|
||||
guard case .xmodemPacket(let v)? = self.payloadVariant else { preconditionFailure() }
|
||||
try visitor.visitSingularMessageField(value: v, fieldNumber: 5)
|
||||
}()
|
||||
case nil: break
|
||||
}
|
||||
try unknownFields.traverse(visitor: &visitor)
|
||||
|
|
|
|||
75
Meshtastic/Protobufs/rtttl.pb.swift
Normal file
75
Meshtastic/Protobufs/rtttl.pb.swift
Normal file
|
|
@ -0,0 +1,75 @@
|
|||
// DO NOT EDIT.
|
||||
// swift-format-ignore-file
|
||||
//
|
||||
// Generated by the Swift generator plugin for the protocol buffer compiler.
|
||||
// Source: rtttl.proto
|
||||
//
|
||||
// For information on using the generated types, please see the documentation:
|
||||
// https://github.com/apple/swift-protobuf/
|
||||
|
||||
import Foundation
|
||||
import SwiftProtobuf
|
||||
|
||||
// If the compiler emits an error on this type, it is because this file
|
||||
// was generated by a version of the `protoc` Swift plug-in that is
|
||||
// incompatible with the version of SwiftProtobuf to which you are linking.
|
||||
// Please ensure that you are building against the same version of the API
|
||||
// that was used to generate this file.
|
||||
fileprivate struct _GeneratedWithProtocGenSwiftVersion: SwiftProtobuf.ProtobufAPIVersionCheck {
|
||||
struct _2: SwiftProtobuf.ProtobufAPIVersion_2 {}
|
||||
typealias Version = _2
|
||||
}
|
||||
|
||||
///
|
||||
/// Canned message module configuration.
|
||||
struct RTTTLConfig {
|
||||
// SwiftProtobuf.Message conformance is added in an extension below. See the
|
||||
// `Message` and `Message+*Additions` files in the SwiftProtobuf library for
|
||||
// methods supported on all messages.
|
||||
|
||||
///
|
||||
/// Ringtone for PWM Buzzer in RTTTL Format.
|
||||
var ringtone: String = String()
|
||||
|
||||
var unknownFields = SwiftProtobuf.UnknownStorage()
|
||||
|
||||
init() {}
|
||||
}
|
||||
|
||||
#if swift(>=5.5) && canImport(_Concurrency)
|
||||
extension RTTTLConfig: @unchecked Sendable {}
|
||||
#endif // swift(>=5.5) && canImport(_Concurrency)
|
||||
|
||||
// MARK: - Code below here is support for the SwiftProtobuf runtime.
|
||||
|
||||
extension RTTTLConfig: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding {
|
||||
static let protoMessageName: String = "RTTTLConfig"
|
||||
static let _protobuf_nameMap: SwiftProtobuf._NameMap = [
|
||||
1: .same(proto: "ringtone"),
|
||||
]
|
||||
|
||||
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.decodeSingularStringField(value: &self.ringtone) }()
|
||||
default: break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func traverse<V: SwiftProtobuf.Visitor>(visitor: inout V) throws {
|
||||
if !self.ringtone.isEmpty {
|
||||
try visitor.visitSingularStringField(value: self.ringtone, fieldNumber: 1)
|
||||
}
|
||||
try unknownFields.traverse(visitor: &visitor)
|
||||
}
|
||||
|
||||
static func ==(lhs: RTTTLConfig, rhs: RTTTLConfig) -> Bool {
|
||||
if lhs.ringtone != rhs.ringtone {return false}
|
||||
if lhs.unknownFields != rhs.unknownFields {return false}
|
||||
return true
|
||||
}
|
||||
}
|
||||
173
Meshtastic/Protobufs/xmodem.pb.swift
Normal file
173
Meshtastic/Protobufs/xmodem.pb.swift
Normal file
|
|
@ -0,0 +1,173 @@
|
|||
// DO NOT EDIT.
|
||||
// swift-format-ignore-file
|
||||
//
|
||||
// Generated by the Swift generator plugin for the protocol buffer compiler.
|
||||
// Source: xmodem.proto
|
||||
//
|
||||
// For information on using the generated types, please see the documentation:
|
||||
// https://github.com/apple/swift-protobuf/
|
||||
|
||||
import Foundation
|
||||
import SwiftProtobuf
|
||||
|
||||
// If the compiler emits an error on this type, it is because this file
|
||||
// was generated by a version of the `protoc` Swift plug-in that is
|
||||
// incompatible with the version of SwiftProtobuf to which you are linking.
|
||||
// Please ensure that you are building against the same version of the API
|
||||
// that was used to generate this file.
|
||||
fileprivate struct _GeneratedWithProtocGenSwiftVersion: SwiftProtobuf.ProtobufAPIVersionCheck {
|
||||
struct _2: SwiftProtobuf.ProtobufAPIVersion_2 {}
|
||||
typealias Version = _2
|
||||
}
|
||||
|
||||
struct XModem {
|
||||
// SwiftProtobuf.Message conformance is added in an extension below. See the
|
||||
// `Message` and `Message+*Additions` files in the SwiftProtobuf library for
|
||||
// methods supported on all messages.
|
||||
|
||||
var control: XModem.Control = .nul
|
||||
|
||||
var seq: UInt32 = 0
|
||||
|
||||
var crc16: UInt32 = 0
|
||||
|
||||
var buffer: Data = Data()
|
||||
|
||||
var unknownFields = SwiftProtobuf.UnknownStorage()
|
||||
|
||||
enum Control: SwiftProtobuf.Enum {
|
||||
typealias RawValue = Int
|
||||
case nul // = 0
|
||||
case soh // = 1
|
||||
case stx // = 2
|
||||
case eot // = 4
|
||||
case ack // = 6
|
||||
case nak // = 21
|
||||
case can // = 24
|
||||
case ctrlz // = 26
|
||||
case UNRECOGNIZED(Int)
|
||||
|
||||
init() {
|
||||
self = .nul
|
||||
}
|
||||
|
||||
init?(rawValue: Int) {
|
||||
switch rawValue {
|
||||
case 0: self = .nul
|
||||
case 1: self = .soh
|
||||
case 2: self = .stx
|
||||
case 4: self = .eot
|
||||
case 6: self = .ack
|
||||
case 21: self = .nak
|
||||
case 24: self = .can
|
||||
case 26: self = .ctrlz
|
||||
default: self = .UNRECOGNIZED(rawValue)
|
||||
}
|
||||
}
|
||||
|
||||
var rawValue: Int {
|
||||
switch self {
|
||||
case .nul: return 0
|
||||
case .soh: return 1
|
||||
case .stx: return 2
|
||||
case .eot: return 4
|
||||
case .ack: return 6
|
||||
case .nak: return 21
|
||||
case .can: return 24
|
||||
case .ctrlz: return 26
|
||||
case .UNRECOGNIZED(let i): return i
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
init() {}
|
||||
}
|
||||
|
||||
#if swift(>=4.2)
|
||||
|
||||
extension XModem.Control: CaseIterable {
|
||||
// The compiler won't synthesize support with the UNRECOGNIZED case.
|
||||
static var allCases: [XModem.Control] = [
|
||||
.nul,
|
||||
.soh,
|
||||
.stx,
|
||||
.eot,
|
||||
.ack,
|
||||
.nak,
|
||||
.can,
|
||||
.ctrlz,
|
||||
]
|
||||
}
|
||||
|
||||
#endif // swift(>=4.2)
|
||||
|
||||
#if swift(>=5.5) && canImport(_Concurrency)
|
||||
extension XModem: @unchecked Sendable {}
|
||||
extension XModem.Control: @unchecked Sendable {}
|
||||
#endif // swift(>=5.5) && canImport(_Concurrency)
|
||||
|
||||
// MARK: - Code below here is support for the SwiftProtobuf runtime.
|
||||
|
||||
extension XModem: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding {
|
||||
static let protoMessageName: String = "XModem"
|
||||
static let _protobuf_nameMap: SwiftProtobuf._NameMap = [
|
||||
1: .same(proto: "control"),
|
||||
2: .same(proto: "seq"),
|
||||
3: .same(proto: "crc16"),
|
||||
4: .same(proto: "buffer"),
|
||||
]
|
||||
|
||||
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.decodeSingularEnumField(value: &self.control) }()
|
||||
case 2: try { try decoder.decodeSingularUInt32Field(value: &self.seq) }()
|
||||
case 3: try { try decoder.decodeSingularUInt32Field(value: &self.crc16) }()
|
||||
case 4: try { try decoder.decodeSingularBytesField(value: &self.buffer) }()
|
||||
default: break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func traverse<V: SwiftProtobuf.Visitor>(visitor: inout V) throws {
|
||||
if self.control != .nul {
|
||||
try visitor.visitSingularEnumField(value: self.control, fieldNumber: 1)
|
||||
}
|
||||
if self.seq != 0 {
|
||||
try visitor.visitSingularUInt32Field(value: self.seq, fieldNumber: 2)
|
||||
}
|
||||
if self.crc16 != 0 {
|
||||
try visitor.visitSingularUInt32Field(value: self.crc16, fieldNumber: 3)
|
||||
}
|
||||
if !self.buffer.isEmpty {
|
||||
try visitor.visitSingularBytesField(value: self.buffer, fieldNumber: 4)
|
||||
}
|
||||
try unknownFields.traverse(visitor: &visitor)
|
||||
}
|
||||
|
||||
static func ==(lhs: XModem, rhs: XModem) -> Bool {
|
||||
if lhs.control != rhs.control {return false}
|
||||
if lhs.seq != rhs.seq {return false}
|
||||
if lhs.crc16 != rhs.crc16 {return false}
|
||||
if lhs.buffer != rhs.buffer {return false}
|
||||
if lhs.unknownFields != rhs.unknownFields {return false}
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
extension XModem.Control: SwiftProtobuf._ProtoNameProviding {
|
||||
static let _protobuf_nameMap: SwiftProtobuf._NameMap = [
|
||||
0: .same(proto: "NUL"),
|
||||
1: .same(proto: "SOH"),
|
||||
2: .same(proto: "STX"),
|
||||
4: .same(proto: "EOT"),
|
||||
6: .same(proto: "ACK"),
|
||||
21: .same(proto: "NAK"),
|
||||
24: .same(proto: "CAN"),
|
||||
26: .same(proto: "CTRLZ"),
|
||||
]
|
||||
}
|
||||
|
|
@ -111,7 +111,7 @@ struct Connect: View {
|
|||
if isUnsetRegion {
|
||||
HStack {
|
||||
NavigationLink {
|
||||
LoRaConfig(node: node)
|
||||
LoRaConfig(node: node, connectedNode: node)
|
||||
} label: {
|
||||
Label("set.region", systemImage: "globe.americas.fill")
|
||||
.foregroundColor(.red)
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@ import MapKit
|
|||
struct DistanceText: View {
|
||||
|
||||
var meters: CLLocationDistance
|
||||
|
||||
|
||||
var body: some View {
|
||||
|
||||
let distanceFormatter = MKDistanceFormatter()
|
||||
|
|
@ -23,7 +23,6 @@ struct DistanceText_Previews: PreviewProvider {
|
|||
static var previews: some View {
|
||||
|
||||
VStack {
|
||||
|
||||
DistanceText(meters: 100)
|
||||
DistanceText(meters: 1000)
|
||||
DistanceText(meters: 10000)
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
// LocalMBTileOverlay.swift
|
||||
// MeshtasticApple
|
||||
//
|
||||
// Created by Joshua Pirihi on 16/01/22.
|
||||
// Copyright(c) Joshua Pirihi 16/01/22.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
|
|
@ -41,9 +41,7 @@ enum MapTileError: Error {
|
|||
class LocalMBTileOverlay: MKTileOverlay {
|
||||
|
||||
var path: String!
|
||||
|
||||
var mb: Connection!
|
||||
|
||||
private var _boundingMapRect: MKMapRect!
|
||||
override var boundingMapRect: MKMapRect {
|
||||
get {
|
||||
|
|
@ -55,7 +53,6 @@ class LocalMBTileOverlay: MKTileOverlay {
|
|||
|
||||
super.init(urlTemplate: nil)
|
||||
self.path = path
|
||||
|
||||
do {
|
||||
self.mb = try Connection(self.path, readonly: true)
|
||||
let metadata = Table("metadata")
|
||||
|
|
@ -87,43 +84,29 @@ class LocalMBTileOverlay: MKTileOverlay {
|
|||
]
|
||||
self._boundingMapRect = MKMapRect(coordinates: coords)
|
||||
|
||||
|
||||
} catch {
|
||||
print("💥 Map tile error: \(error)")
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
override func loadTile(at path: MKTileOverlayPath, result: @escaping (Data?, Error?) -> Void) {
|
||||
|
||||
//}
|
||||
|
||||
//override func loadTile(at path: MKTileOverlayPath) async throws -> Data {
|
||||
|
||||
let tileX = Int64(path.x)
|
||||
let tileY = Int64(path.y)
|
||||
let tileZ = Int64(path.z)
|
||||
|
||||
let tileZ = Int64(path.z)
|
||||
let tileData = Expression<SQLite.Blob>("tile_data")
|
||||
let zoomLevel = Expression<Int64>("zoom_level")
|
||||
let tileColumn = Expression<Int64>("tile_column")
|
||||
let tileRow = Expression<Int64>("tile_row")
|
||||
|
||||
if let dataQuery = try? self.mb.pluck(Table("tiles").select(tileData).filter(zoomLevel == tileZ).filter(tileColumn == tileX).filter(tileRow == tileY)) {
|
||||
|
||||
let data = Data(bytes: dataQuery[tileData].bytes, count: dataQuery[tileData].bytes.count)//dataQuery![tileData].bytes
|
||||
|
||||
//return data
|
||||
result(data, nil)
|
||||
|
||||
} else {
|
||||
print("💥 No tile here: x:\(tileX) y:\(tileY) z:\(tileZ)")
|
||||
//return Data()
|
||||
let error = NSError(domain: "LocalMBTileOverlay", code: 1, userInfo: ["reason": "no_tile"])
|
||||
result(nil, error)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
37
Meshtastic/Views/Map/Custom/MapViewFitExtension.swift
Normal file
37
Meshtastic/Views/Map/Custom/MapViewFitExtension.swift
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
//
|
||||
// MapViewFitExtension.swift
|
||||
// Meshtastic
|
||||
//
|
||||
// Created by Garth Vander Houwen on 1/15/23.
|
||||
//
|
||||
|
||||
import MapKit
|
||||
|
||||
extension MKMapView {
|
||||
|
||||
func fitAllAnnotations(with padding: UIEdgeInsets = UIEdgeInsets(top: 100, left: 100, bottom: 100, right: 100)) {
|
||||
var zoomRect: MKMapRect = .null
|
||||
annotations.forEach({
|
||||
let annotationPoint = MKMapPoint($0.coordinate)
|
||||
let pointRect = MKMapRect(x: annotationPoint.x, y: annotationPoint.y, width: 0.01, height: 0.01)
|
||||
zoomRect = zoomRect.union(pointRect)
|
||||
})
|
||||
|
||||
setVisibleMapRect(zoomRect, edgePadding: padding, animated: true)
|
||||
}
|
||||
|
||||
func fit(annotations: [MKAnnotation], andShow show: Bool, with padding: UIEdgeInsets = UIEdgeInsets(top: 100, left: 100, bottom: 100, right: 100)) {
|
||||
var zoomRect: MKMapRect = .null
|
||||
annotations.forEach({
|
||||
let aPoint = MKMapPoint($0.coordinate)
|
||||
let rect = MKMapRect(x: aPoint.x, y: aPoint.y, width: 0.1, height: 0.1)
|
||||
zoomRect = zoomRect.isNull ? rect : zoomRect.union(rect)
|
||||
})
|
||||
|
||||
if show {
|
||||
addAnnotations(annotations)
|
||||
}
|
||||
|
||||
setVisibleMapRect(zoomRect, edgePadding: padding, animated: true)
|
||||
}
|
||||
}
|
||||
469
Meshtastic/Views/Map/Custom/MapViewModule.swift
Normal file
469
Meshtastic/Views/Map/Custom/MapViewModule.swift
Normal file
|
|
@ -0,0 +1,469 @@
|
|||
////
|
||||
//// MapView.swift
|
||||
//// MapViewTest
|
||||
////
|
||||
//// Created by Cem Yilmaz on 05.07.21.
|
||||
////
|
||||
//import SwiftUI
|
||||
//import MapKit
|
||||
//import CoreData
|
||||
//
|
||||
//#if canImport(MapKit) && canImport(UIKit)
|
||||
//public struct MapView: UIViewRepresentable {
|
||||
//
|
||||
// @Environment(\.managedObjectContext) var context
|
||||
//
|
||||
// //var context: NSManagedObjectContext?
|
||||
//
|
||||
// //@Binding private var region: MKCoordinateRegion
|
||||
//
|
||||
// //make this view dependent on the UserDefault that is updated when importing a new map file
|
||||
// @AppStorage("lastUpdatedLocalMapFile") private var lastUpdatedLocalMapFile = 0
|
||||
// @State private var loadedLastUpdatedLocalMapFile = 0
|
||||
//
|
||||
// private var customMapOverlay: CustomMapOverlay?
|
||||
// @State private var presentCustomMapOverlayHash: CustomMapOverlay?
|
||||
//
|
||||
// private var mapType: MKMapType
|
||||
//
|
||||
// private var showZoomScale: Bool
|
||||
// private var zoomEnabled: Bool
|
||||
// private var zoomRange: (minHeight: CLLocationDistance?, maxHeight: CLLocationDistance?)
|
||||
//
|
||||
// private var scrollEnabled: Bool
|
||||
// private var scrollBoundaries: MKCoordinateRegion?
|
||||
//
|
||||
// private var rotationEnabled: Bool
|
||||
// private var showCompassWhenRotated: Bool
|
||||
//
|
||||
// private var showUserLocation: Bool
|
||||
// private var userTrackingMode: MKUserTrackingMode
|
||||
// @Binding private var userLocation: CLLocationCoordinate2D?
|
||||
//
|
||||
// private var overlays: [Overlay]
|
||||
//
|
||||
// @FetchRequest(sortDescriptors: [NSSortDescriptor(key: "time", ascending: false)], animation: .default)
|
||||
// private var positions: FetchedResults<PositionEntity>
|
||||
//
|
||||
// public init(
|
||||
// customMapOverlay: CustomMapOverlay? = nil,
|
||||
// mapType: String = "hybrid",
|
||||
// zoomEnabled: Bool = true,
|
||||
// showZoomScale: Bool = false,
|
||||
// zoomRange: (minHeight: CLLocationDistance?, maxHeight: CLLocationDistance?) = (nil, nil),
|
||||
// scrollEnabled: Bool = true,
|
||||
// scrollBoundaries: MKCoordinateRegion? = nil,
|
||||
// rotationEnabled: Bool = true,
|
||||
// showCompassWhenRotated: Bool = true,
|
||||
// showUserLocation: Bool = true,
|
||||
// userTrackingMode: MKUserTrackingMode = MKUserTrackingMode.none,
|
||||
// userLocation: Binding<CLLocationCoordinate2D?> = .constant(nil),
|
||||
// overlays: [Overlay] = []
|
||||
// ) {
|
||||
// self.customMapOverlay = customMapOverlay
|
||||
//
|
||||
// switch mapType {
|
||||
// case "satellite":
|
||||
// self.mapType = .satellite
|
||||
// break
|
||||
// case "standard":
|
||||
// self.mapType = .standard
|
||||
// break
|
||||
// case "hybrid":
|
||||
// self.mapType = .hybrid
|
||||
// break
|
||||
// default:
|
||||
// self.mapType = .hybrid
|
||||
// }
|
||||
//
|
||||
// self.showZoomScale = showZoomScale
|
||||
// self.zoomEnabled = zoomEnabled
|
||||
// self.zoomRange = zoomRange
|
||||
//
|
||||
// self.scrollEnabled = scrollEnabled
|
||||
// self.scrollBoundaries = scrollBoundaries
|
||||
//
|
||||
// self.rotationEnabled = rotationEnabled
|
||||
// self.showCompassWhenRotated = showCompassWhenRotated
|
||||
//
|
||||
// self.showUserLocation = showUserLocation
|
||||
// self.userTrackingMode = userTrackingMode
|
||||
// self._userLocation = userLocation
|
||||
//
|
||||
// self.overlays = overlays
|
||||
//
|
||||
// }
|
||||
//
|
||||
// public func makeUIView(context: Context) -> MKMapView {
|
||||
// let mapView = MKMapView()
|
||||
// mapView.delegate = context.coordinator
|
||||
// mapView.register(PositionAnnotationView.self, forAnnotationViewWithReuseIdentifier: NSStringFromClass(PositionAnnotationView.self))
|
||||
//
|
||||
// return mapView
|
||||
// }
|
||||
//
|
||||
//
|
||||
// public func updateUIView(_ mapView: MKMapView, context: Context) {
|
||||
//
|
||||
// if self.customMapOverlay != self.presentCustomMapOverlayHash || self.loadedLastUpdatedLocalMapFile != self.lastUpdatedLocalMapFile {
|
||||
// mapView.removeOverlays(mapView.overlays)
|
||||
// if let customMapOverlay = self.customMapOverlay {
|
||||
//
|
||||
// let fileManager = FileManager.default
|
||||
// let documentsDirectory = fileManager.urls(for: .documentDirectory, in: .userDomainMask).first!
|
||||
// let tilePath = documentsDirectory.appendingPathComponent("offline_map.mbtiles", isDirectory: false).path
|
||||
// if fileManager.fileExists(atPath: tilePath) {
|
||||
// //if let tilePath = Bundle.main.path(forResource: "offline_map", ofType: "mbtiles") {
|
||||
//
|
||||
// print("Loading local map file")
|
||||
//
|
||||
// if let overlay = LocalMBTileOverlay(mbTilePath: tilePath) {
|
||||
//
|
||||
// overlay.canReplaceMapContent = false//customMapOverlay.canReplaceMapContent
|
||||
//
|
||||
// mapView.addOverlay(overlay)
|
||||
// }
|
||||
// } else {
|
||||
// print("Couldn't find a local map file to load")
|
||||
// }
|
||||
// }
|
||||
// DispatchQueue.main.async {
|
||||
// self.presentCustomMapOverlayHash = self.customMapOverlay
|
||||
// self.loadedLastUpdatedLocalMapFile = self.lastUpdatedLocalMapFile
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// if mapView.mapType != self.mapType {
|
||||
// mapView.mapType = self.mapType
|
||||
// }
|
||||
//
|
||||
// mapView.showsScale = self.zoomEnabled ? self.showZoomScale : false
|
||||
//
|
||||
// if mapView.isZoomEnabled != self.zoomEnabled {
|
||||
// mapView.isZoomEnabled = self.zoomEnabled
|
||||
// }
|
||||
//
|
||||
// if mapView.cameraZoomRange.minCenterCoordinateDistance != self.zoomRange.minHeight ?? 0 ||
|
||||
// mapView.cameraZoomRange.maxCenterCoordinateDistance != self.zoomRange.maxHeight ?? .infinity {
|
||||
// mapView.cameraZoomRange = MKMapView.CameraZoomRange(
|
||||
// minCenterCoordinateDistance: self.zoomRange.minHeight ?? 0,
|
||||
// maxCenterCoordinateDistance: self.zoomRange.maxHeight ?? .infinity
|
||||
// )
|
||||
// }
|
||||
//
|
||||
// mapView.isScrollEnabled = self.userTrackingMode == MKUserTrackingMode.none ? self.scrollEnabled : false
|
||||
//
|
||||
// if let scrollBoundary = self.scrollBoundaries, (mapView.cameraBoundary?.region.center.latitude != scrollBoundary.center.latitude || mapView.cameraBoundary?.region.center.longitude != scrollBoundary.center.longitude || mapView.camera Boundary?.region.span.latitudeDelta != scrollBoundary.span.latitudeDelta || mapView.cameraBoundary?.region.span.longitudeDelta != scrollBoundary.span.longitudeDelta) {
|
||||
// mapView.cameraBoundary = MKMapView.CameraBoundary(coordinateRegion: scrollBoundary)
|
||||
// } else if self.scrollBoundaries == nil && mapView.cameraBoundary != nil {
|
||||
// mapView.cameraBoundary = nil
|
||||
// }
|
||||
//
|
||||
// mapView.isRotateEnabled = self.userTrackingMode != .followWithHeading ? self.rotationEnabled : false
|
||||
// mapView.showsCompass = self.userTrackingMode != .followWithHeading ? self.showCompassWhenRotated : false
|
||||
//
|
||||
// if mapView.showsUserLocation != self.showUserLocation {
|
||||
// mapView.showsUserLocation = self.showUserLocation
|
||||
// }
|
||||
//
|
||||
// if mapView.userTrackingMode != self.userTrackingMode {
|
||||
// mapView.userTrackingMode = self.userTrackingMode
|
||||
// }
|
||||
//
|
||||
// // clear any existing annotations
|
||||
// var shouldMoveRegion = false
|
||||
// if !mapView.annotations.isEmpty {
|
||||
// mapView.removeAnnotations(mapView.annotations)
|
||||
// } else {
|
||||
// shouldMoveRegion = true
|
||||
// }
|
||||
//
|
||||
// var displayedNodes: [Int64] = []
|
||||
// for position in self.positions {
|
||||
// if position.nodePosition == nil || displayedNodes.contains(position.nodePosition!.num) || position.coordinate == nil {
|
||||
// continue
|
||||
// }
|
||||
//
|
||||
// let annotation = PositionAnnotation()
|
||||
// annotation.coordinate = position.nodeCoordinate!
|
||||
// annotation.title = position.nodePosition!.user?.longName ?? NSLocalizedString("unknown", comment: "Unknown")
|
||||
// annotation.shortName = position.nodePosition!.user?.shortName?.uppercased() ?? "???"
|
||||
//
|
||||
// mapView.addAnnotation(annotation)
|
||||
//
|
||||
// displayedNodes.append(position.nodePosition!.num)
|
||||
// }
|
||||
//
|
||||
// if shouldMoveRegion {
|
||||
// self.moveToMeshRegion(mapView)
|
||||
// }
|
||||
//
|
||||
//
|
||||
// }
|
||||
//
|
||||
// func moveToMeshRegion(_ mapView: MKMapView) {
|
||||
// //go through the annotations and create a bounding box that encloses them
|
||||
//
|
||||
// var minLat: CLLocationDegrees = 90.0
|
||||
// var maxLat: CLLocationDegrees = -90.0
|
||||
// var minLon: CLLocationDegrees = 180.0
|
||||
// var maxLon: CLLocationDegrees = -180.0
|
||||
//
|
||||
// for annotation in mapView.annotations {
|
||||
// if annotation.isKind(of: PositionAnnotation.self) {
|
||||
// minLat = min(minLat, annotation.coordinate.latitude)
|
||||
// maxLat = max(maxLat, annotation.coordinate.latitude)
|
||||
// minLon = min(minLon, annotation.coordinate.longitude)
|
||||
// maxLon = max(maxLon, annotation.coordinate.longitude)
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// //check if the mesh region looks sensible before we move to it. Otherwise we won't move the map (leave it at the current location)
|
||||
// if maxLat < minLat || (maxLat-minLat) > 5 || maxLon < minLon || (maxLon-minLon) > 5 {
|
||||
// return
|
||||
// } else if minLat == maxLat && minLon == maxLon {
|
||||
// //then we are focussed on a single point (probably because there is only one node with a position)
|
||||
// //widen that out a little (don't zoom way in to that point)
|
||||
//
|
||||
// //0.001 degrees latitude is about 100m
|
||||
// //the mapView.regionThatFits call below will expand this out to a rectangle
|
||||
// minLat = minLat - 0.001
|
||||
// maxLat = maxLat + 0.001
|
||||
// }
|
||||
//
|
||||
// let centerCoord = CLLocationCoordinate2D(latitude: (minLat+maxLat)/2, longitude: (minLon+maxLon)/2)
|
||||
//
|
||||
// let span = MKCoordinateSpan(latitudeDelta: (maxLat-minLat)*1.5, longitudeDelta: (maxLon-minLon)*1.5)
|
||||
//
|
||||
// let region = mapView.regionThatFits(MKCoordinateRegion(center: centerCoord, span: span))
|
||||
//
|
||||
// mapView.setRegion(region, animated: true)
|
||||
// }
|
||||
//
|
||||
// public func makeCoordinator() -> Coordinator {
|
||||
// Coordinator(parent: self)
|
||||
// }
|
||||
//
|
||||
// public class Coordinator: NSObject, MKMapViewDelegate {
|
||||
//
|
||||
// private var parent: MapView
|
||||
// public var overlays: [Overlay] = []
|
||||
//
|
||||
// init(parent: MapView) {
|
||||
// self.parent = parent
|
||||
// }
|
||||
//
|
||||
// public func mapView(_ mapView: MKMapView, viewFor annotation: MKAnnotation) -> MKAnnotationView? {
|
||||
//
|
||||
// guard !annotation.isKind(of: MKUserLocation.self) else {
|
||||
// // Make a fast exit if the annotation is the `MKUserLocation`, as it's not an annotation view we wish to customize.
|
||||
// return nil
|
||||
// }
|
||||
//
|
||||
// if let annotation = annotation as? PositionAnnotation {
|
||||
//
|
||||
// let annotationView = PositionAnnotationView(annotation: annotation, reuseIdentifier: "PositionAnnotation")
|
||||
// annotationView.name = annotation.shortName ?? "????"
|
||||
// annotationView.canShowCallout = true
|
||||
//
|
||||
// return annotationView
|
||||
// }
|
||||
//
|
||||
// return nil
|
||||
// }
|
||||
//
|
||||
// public func mapView(_ mapView: MKMapView, rendererFor overlay: MKOverlay) -> MKOverlayRenderer {
|
||||
//
|
||||
// if let index = self.overlays.firstIndex(where: { overlay_ in overlay_.shape.hash == overlay.hash }) {
|
||||
//
|
||||
// let unwrappedOverlay = self.overlays[index]
|
||||
//
|
||||
// if let circleOverlay = unwrappedOverlay.shape as? MKCircle {
|
||||
//
|
||||
// let renderer = MKCircleRenderer(circle: circleOverlay)
|
||||
// renderer.fillColor = unwrappedOverlay.fillColor
|
||||
// renderer.strokeColor = unwrappedOverlay.strokeColor
|
||||
// renderer.lineWidth = unwrappedOverlay.lineWidth
|
||||
// return renderer
|
||||
//
|
||||
// } else if let polygonOverlay = unwrappedOverlay.shape as? MKPolygon {
|
||||
//
|
||||
// let renderer = MKPolygonRenderer(polygon: polygonOverlay)
|
||||
// renderer.fillColor = unwrappedOverlay.fillColor
|
||||
// renderer.strokeColor = unwrappedOverlay.strokeColor
|
||||
// renderer.lineWidth = unwrappedOverlay.lineWidth
|
||||
// return renderer
|
||||
//
|
||||
// } else if let multiPolygonOverlay = unwrappedOverlay.shape as? MKMultiPolygon {
|
||||
//
|
||||
// let renderer = MKMultiPolygonRenderer(multiPolygon: multiPolygonOverlay)
|
||||
// renderer.fillColor = unwrappedOverlay.fillColor
|
||||
// renderer.strokeColor = unwrappedOverlay.strokeColor
|
||||
// renderer.lineWidth = unwrappedOverlay.lineWidth
|
||||
// return renderer
|
||||
//
|
||||
// } else if let polyLineOverlay = unwrappedOverlay.shape as? MKPolyline {
|
||||
//
|
||||
// let renderer = MKPolylineRenderer(polyline: polyLineOverlay)
|
||||
// renderer.fillColor = unwrappedOverlay.fillColor
|
||||
// renderer.strokeColor = unwrappedOverlay.strokeColor
|
||||
// renderer.lineWidth = unwrappedOverlay.lineWidth
|
||||
// return renderer
|
||||
//
|
||||
// } else if let multiPolylineOverlay = unwrappedOverlay.shape as? MKMultiPolyline {
|
||||
//
|
||||
// let renderer = MKMultiPolylineRenderer(multiPolyline: multiPolylineOverlay)
|
||||
// renderer.fillColor = unwrappedOverlay.fillColor
|
||||
// renderer.strokeColor = unwrappedOverlay.strokeColor
|
||||
// renderer.lineWidth = unwrappedOverlay.lineWidth
|
||||
// return renderer
|
||||
//
|
||||
// } else {
|
||||
//
|
||||
// return MKOverlayRenderer()
|
||||
//
|
||||
// }
|
||||
//
|
||||
// } else if let tileOverlay = overlay as? MKTileOverlay {
|
||||
//
|
||||
// return MKTileOverlayRenderer(tileOverlay: tileOverlay)
|
||||
//
|
||||
// } else {
|
||||
// return MKOverlayRenderer()
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// /// is supposed to be located in the folder with the map name
|
||||
// public struct DefaultTile: Hashable {
|
||||
// let tileName: String
|
||||
// let tileType: String
|
||||
//
|
||||
// public init(tileName: String, tileType: String) {
|
||||
// self.tileName = tileName
|
||||
// self.tileType = tileType
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// public struct CustomMapOverlay: Equatable, Hashable {
|
||||
// let mapName: String
|
||||
// let tileType: String
|
||||
// var canReplaceMapContent: Bool
|
||||
// var minimumZoomLevel: Int?
|
||||
// var maximumZoomLevel: Int?
|
||||
// let defaultTile: DefaultTile?
|
||||
//
|
||||
// public init(
|
||||
// mapName: String,
|
||||
// tileType: String,
|
||||
// canReplaceMapContent: Bool = true, // false for transparent tiles
|
||||
// minimumZoomLevel: Int? = nil,
|
||||
// maximumZoomLevel: Int? = nil,
|
||||
// defaultTile: DefaultTile? = nil
|
||||
// ) {
|
||||
//
|
||||
// self.mapName = mapName
|
||||
// self.tileType = tileType
|
||||
// self.canReplaceMapContent = canReplaceMapContent
|
||||
// self.minimumZoomLevel = minimumZoomLevel
|
||||
// self.maximumZoomLevel = maximumZoomLevel
|
||||
// self.defaultTile = defaultTile
|
||||
// }
|
||||
//
|
||||
// public init?(
|
||||
// mapName: String?,
|
||||
// tileType: String,
|
||||
// canReplaceMapContent: Bool = true, // false for transparent tiles
|
||||
// minimumZoomLevel: Int? = nil,
|
||||
// maximumZoomLevel: Int? = nil,
|
||||
// defaultTile: DefaultTile? = nil
|
||||
// ) {
|
||||
// if (mapName == nil || mapName! == "") {
|
||||
// return nil
|
||||
// }
|
||||
// self.mapName = mapName!
|
||||
// self.tileType = tileType
|
||||
// self.canReplaceMapContent = canReplaceMapContent
|
||||
// self.minimumZoomLevel = minimumZoomLevel
|
||||
// self.maximumZoomLevel = maximumZoomLevel
|
||||
// self.defaultTile = defaultTile
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// public class CustomMapOverlaySource: MKTileOverlay {
|
||||
//
|
||||
// // requires folder: tiles/{mapName}/z/y/y,{tileType}
|
||||
// private var parent: MapView
|
||||
// private let mapName: String
|
||||
// private let tileType: String
|
||||
// private let defaultTile: DefaultTile?
|
||||
//
|
||||
// public init(
|
||||
// parent: MapView,
|
||||
// mapName: String,
|
||||
// tileType: String,
|
||||
// defaultTile: DefaultTile?
|
||||
// ) {
|
||||
// self.parent = parent
|
||||
// self.mapName = mapName
|
||||
// self.tileType = tileType
|
||||
// self.defaultTile = defaultTile
|
||||
// super.init(urlTemplate: "")
|
||||
// }
|
||||
//
|
||||
// public override func url(forTilePath path: MKTileOverlayPath) -> URL {
|
||||
// if let tileUrl = Bundle.main.url(
|
||||
// forResource: "\(path.y)",
|
||||
// withExtension: self.tileType,
|
||||
// subdirectory: "tiles/\(self.mapName)/\(path.z)/\(path.x)",
|
||||
// localization: nil
|
||||
// ) {
|
||||
// return tileUrl
|
||||
// } else if let defaultTile = self.defaultTile, let defaultTileUrl = Bundle.main.url(
|
||||
// forResource: defaultTile.tileName,
|
||||
// withExtension: defaultTile.tileType,
|
||||
// subdirectory: "tiles/\(self.mapName)",
|
||||
// localization: nil
|
||||
// ) {
|
||||
// return defaultTileUrl
|
||||
// } else {
|
||||
// let urlstring = self.mapName+"\(path.z)/\(path.x)/\(path.y).png"
|
||||
// return URL(string: urlstring)!
|
||||
// // Bundle.main.url(forResource: "surrounding", withExtension: "png", subdirectory: "tiles")!
|
||||
// }
|
||||
//
|
||||
// }
|
||||
//
|
||||
// }
|
||||
//
|
||||
// public struct Overlay {
|
||||
//
|
||||
// public static func == (lhs: MapView.Overlay, rhs: MapView.Overlay) -> Bool {
|
||||
// // maybe to use in the future for comparison of full array
|
||||
// lhs.shape.coordinate.latitude == rhs.shape.coordinate.latitude &&
|
||||
// lhs.shape.coordinate.longitude == rhs.shape.coordinate.longitude &&
|
||||
// lhs.fillColor == rhs.fillColor
|
||||
// }
|
||||
//
|
||||
// var shape: MKOverlay
|
||||
// var fillColor: UIColor?
|
||||
// var strokeColor: UIColor?
|
||||
// var lineWidth: CGFloat
|
||||
//
|
||||
// public init(
|
||||
// shape: MKOverlay,
|
||||
// fillColor: UIColor? = nil,
|
||||
// strokeColor: UIColor? = nil,
|
||||
// lineWidth: CGFloat = 0
|
||||
// ) {
|
||||
// self.shape = shape
|
||||
// self.fillColor = fillColor
|
||||
// self.strokeColor = strokeColor
|
||||
// self.lineWidth = lineWidth
|
||||
// }
|
||||
// }
|
||||
//
|
||||
//}
|
||||
//
|
||||
//// MARK: End of implementation
|
||||
//#endif
|
||||
354
Meshtastic/Views/Map/Custom/MapViewSwiftUI.swift
Normal file
354
Meshtastic/Views/Map/Custom/MapViewSwiftUI.swift
Normal file
|
|
@ -0,0 +1,354 @@
|
|||
//
|
||||
// MapViewSwitUI.swift
|
||||
// Meshtastic
|
||||
//
|
||||
// Copyright(c) Josh Pirihi & Garth Vander Houwen 1/16/22.
|
||||
|
||||
import SwiftUI
|
||||
import MapKit
|
||||
|
||||
struct MapViewSwiftUI: UIViewRepresentable {
|
||||
|
||||
var onLongPress: (_ waypointCoordinate: CLLocationCoordinate2D) -> Void
|
||||
var onWaypointEdit: (_ waypointId: Int ) -> Void
|
||||
let mapView = MKMapView()
|
||||
let positions: [PositionEntity]
|
||||
let waypoints: [WaypointEntity]
|
||||
let mapViewType: MKMapType
|
||||
let centerOnPositionsOnly: Bool
|
||||
|
||||
// Offline Maps
|
||||
//make this view dependent on the UserDefault that is updated when importing a new map file
|
||||
@AppStorage("lastUpdatedLocalMapFile") private var lastUpdatedLocalMapFile = 0
|
||||
@State private var loadedLastUpdatedLocalMapFile = 0
|
||||
var customMapOverlay: CustomMapOverlay?
|
||||
@State private var presentCustomMapOverlayHash: CustomMapOverlay?
|
||||
var overlays: [Overlay] = []
|
||||
let dynamicRegion: Bool = true
|
||||
|
||||
func makeUIView(context: Context) -> MKMapView {
|
||||
// Parameters
|
||||
mapView.addAnnotations(waypoints)
|
||||
if centerOnPositionsOnly {
|
||||
mapView.fit(annotations: positions, andShow: true)
|
||||
} else {
|
||||
mapView.addAnnotations(positions)
|
||||
mapView.fitAllAnnotations()
|
||||
}
|
||||
mapView.mapType = mapViewType
|
||||
mapView.setUserTrackingMode(.none, animated: true)
|
||||
// Other MKMapView Settings
|
||||
mapView.isPitchEnabled = true
|
||||
mapView.isRotateEnabled = true
|
||||
mapView.isScrollEnabled = true
|
||||
mapView.isZoomEnabled = true
|
||||
mapView.showsBuildings = true
|
||||
mapView.showsCompass = true
|
||||
mapView.showsScale = true
|
||||
mapView.showsTraffic = true
|
||||
mapView.showsUserLocation = true
|
||||
#if targetEnvironment(macCatalyst)
|
||||
mapView.showsZoomControls = true
|
||||
#endif
|
||||
mapView.delegate = context.coordinator
|
||||
return mapView
|
||||
}
|
||||
|
||||
func updateUIView(_ mapView: MKMapView, context: Context) {
|
||||
mapView.mapType = mapViewType
|
||||
|
||||
if self.customMapOverlay != self.presentCustomMapOverlayHash || self.loadedLastUpdatedLocalMapFile != self.lastUpdatedLocalMapFile {
|
||||
mapView.removeOverlays(mapView.overlays)
|
||||
if self.customMapOverlay != nil {
|
||||
|
||||
let fileManager = FileManager.default
|
||||
let documentsDirectory = fileManager.urls(for: .documentDirectory, in: .userDomainMask).first!
|
||||
let tilePath = documentsDirectory.appendingPathComponent("offline_map.mbtiles", isDirectory: false).path
|
||||
if fileManager.fileExists(atPath: tilePath) {
|
||||
print("Loading local map file")
|
||||
if let overlay = LocalMBTileOverlay(mbTilePath: tilePath) {
|
||||
overlay.canReplaceMapContent = false//customMapOverlay.canReplaceMapContent
|
||||
mapView.addOverlay(overlay)
|
||||
}
|
||||
} else {
|
||||
print("Couldn't find a local map file to load")
|
||||
}
|
||||
}
|
||||
DispatchQueue.main.async {
|
||||
self.presentCustomMapOverlayHash = self.customMapOverlay
|
||||
self.loadedLastUpdatedLocalMapFile = self.lastUpdatedLocalMapFile
|
||||
}
|
||||
}
|
||||
mapView.removeAnnotations(mapView.annotations)
|
||||
mapView.addAnnotations(positions)
|
||||
mapView.addAnnotations(waypoints)
|
||||
}
|
||||
|
||||
func makeCoordinator() -> MapCoordinator {
|
||||
return Coordinator(self)
|
||||
}
|
||||
|
||||
final class MapCoordinator: NSObject, MKMapViewDelegate, UIGestureRecognizerDelegate {
|
||||
|
||||
var parent: MapViewSwiftUI
|
||||
var longPressRecognizer = UILongPressGestureRecognizer()
|
||||
var overlays: [Overlay] = []
|
||||
|
||||
init(_ parent: MapViewSwiftUI) {
|
||||
self.parent = parent
|
||||
super.init()
|
||||
self.longPressRecognizer = UILongPressGestureRecognizer(target: self, action: #selector(longPressHandler))
|
||||
self.longPressRecognizer.minimumPressDuration = 0.5
|
||||
//self.longPressRecognizer.numberOfTouchesRequired = 1
|
||||
self.longPressRecognizer.cancelsTouchesInView = true
|
||||
self.longPressRecognizer.delegate = self
|
||||
self.parent.mapView.addGestureRecognizer(longPressRecognizer)
|
||||
self.overlays = []
|
||||
}
|
||||
|
||||
func mapView(_ mapView: MKMapView, viewFor annotation: MKAnnotation) -> MKAnnotationView? {
|
||||
|
||||
switch annotation {
|
||||
|
||||
case _ as MKClusterAnnotation:
|
||||
let annotationView = mapView.dequeueReusableAnnotationView(withIdentifier: "nodeGroup") as? MKMarkerAnnotationView ?? MKMarkerAnnotationView(annotation: annotation, reuseIdentifier: "nodeGroup")
|
||||
annotationView.markerTintColor = .brown//.systemRed
|
||||
annotationView.displayPriority = .defaultLow
|
||||
annotationView.tag = -1
|
||||
return annotationView
|
||||
case _ as PositionEntity:
|
||||
let annotationView = mapView.dequeueReusableAnnotationView(withIdentifier: "node") as? MKMarkerAnnotationView ?? MKMarkerAnnotationView(annotation: annotation, reuseIdentifier: "Node")
|
||||
annotationView.tag = -1
|
||||
annotationView.canShowCallout = true
|
||||
annotationView.glyphText = "📟"
|
||||
annotationView.clusteringIdentifier = "nodeGroup"
|
||||
annotationView.markerTintColor = UIColor(.indigo)
|
||||
annotationView.titleVisibility = .adaptive
|
||||
return annotationView
|
||||
case let waypointAnnotation as WaypointEntity:
|
||||
let annotationView = mapView.dequeueReusableAnnotationView(withIdentifier: "waypoint") as? MKMarkerAnnotationView ?? MKMarkerAnnotationView(annotation: annotation, reuseIdentifier: "Waypoint")
|
||||
annotationView.tag = Int(waypointAnnotation.id)
|
||||
annotationView.isEnabled = true
|
||||
annotationView.canShowCallout = true
|
||||
if waypointAnnotation.icon == 0 {
|
||||
annotationView.glyphText = "📍"
|
||||
} else {
|
||||
annotationView.glyphText = String(UnicodeScalar(Int(waypointAnnotation.icon)) ?? "📍")
|
||||
}
|
||||
annotationView.clusteringIdentifier = "waypointGroup"
|
||||
annotationView.markerTintColor = UIColor(.accentColor)
|
||||
annotationView.displayPriority = .required
|
||||
annotationView.titleVisibility = .adaptive
|
||||
let leftIcon = UIImageView(image: annotationView.glyphText?.image())
|
||||
leftIcon.backgroundColor = UIColor(.accentColor)
|
||||
annotationView.leftCalloutAccessoryView = leftIcon
|
||||
let subtitle = UILabel()
|
||||
subtitle.text = waypointAnnotation.longDescription
|
||||
subtitle.numberOfLines = 0
|
||||
annotationView.detailCalloutAccessoryView = subtitle
|
||||
let editIcon = UIButton(type: .detailDisclosure)
|
||||
editIcon.setImage(UIImage(systemName: "square.and.pencil"), for: .normal)
|
||||
annotationView.rightCalloutAccessoryView = editIcon
|
||||
return annotationView
|
||||
default: return nil
|
||||
}
|
||||
}
|
||||
|
||||
func mapView(_ mapView: MKMapView, annotationView view: MKAnnotationView, calloutAccessoryControlTapped control: UIControl) {
|
||||
// Only Allow Edit for waypoint annotations with a id
|
||||
if view.tag > 0 {
|
||||
parent.onWaypointEdit(view.tag)
|
||||
}
|
||||
}
|
||||
|
||||
@objc func longPressHandler(_ gesture: UILongPressGestureRecognizer) {
|
||||
|
||||
if gesture.state != UIGestureRecognizer.State.ended {
|
||||
return
|
||||
} else if gesture.state != UIGestureRecognizer.State.began {
|
||||
|
||||
// Screen Position - CGPoint
|
||||
let location = longPressRecognizer.location(in: self.parent.mapView)
|
||||
|
||||
// Map Coordinate - CLLocationCoordinate2D
|
||||
let coordinate = self.parent.mapView.convert(location, toCoordinateFrom: self.parent.mapView)
|
||||
let annotation = MKPointAnnotation()
|
||||
annotation.title = "Dropped Pin"
|
||||
annotation.coordinate = coordinate
|
||||
parent.mapView.addAnnotation(annotation)
|
||||
UINotificationFeedbackGenerator().notificationOccurred(.success)
|
||||
parent.onLongPress(coordinate)
|
||||
}
|
||||
}
|
||||
|
||||
public func mapView(_ mapView: MKMapView, rendererFor overlay: MKOverlay) -> MKOverlayRenderer {
|
||||
|
||||
if let index = self.overlays.firstIndex(where: { overlay_ in overlay_.shape.hash == overlay.hash }) {
|
||||
|
||||
let unwrappedOverlay = self.overlays[index]
|
||||
if let circleOverlay = unwrappedOverlay.shape as? MKCircle {
|
||||
let renderer = MKCircleRenderer(circle: circleOverlay)
|
||||
renderer.fillColor = unwrappedOverlay.fillColor
|
||||
renderer.strokeColor = unwrappedOverlay.strokeColor
|
||||
renderer.lineWidth = unwrappedOverlay.lineWidth
|
||||
return renderer
|
||||
} else if let polygonOverlay = unwrappedOverlay.shape as? MKPolygon {
|
||||
let renderer = MKPolygonRenderer(polygon: polygonOverlay)
|
||||
renderer.fillColor = unwrappedOverlay.fillColor
|
||||
renderer.strokeColor = unwrappedOverlay.strokeColor
|
||||
renderer.lineWidth = unwrappedOverlay.lineWidth
|
||||
return renderer
|
||||
} else if let multiPolygonOverlay = unwrappedOverlay.shape as? MKMultiPolygon {
|
||||
let renderer = MKMultiPolygonRenderer(multiPolygon: multiPolygonOverlay)
|
||||
renderer.fillColor = unwrappedOverlay.fillColor
|
||||
renderer.strokeColor = unwrappedOverlay.strokeColor
|
||||
renderer.lineWidth = unwrappedOverlay.lineWidth
|
||||
return renderer
|
||||
} else if let polyLineOverlay = unwrappedOverlay.shape as? MKPolyline {
|
||||
let renderer = MKPolylineRenderer(polyline: polyLineOverlay)
|
||||
renderer.fillColor = unwrappedOverlay.fillColor
|
||||
renderer.strokeColor = unwrappedOverlay.strokeColor
|
||||
renderer.lineWidth = unwrappedOverlay.lineWidth
|
||||
return renderer
|
||||
} else if let multiPolylineOverlay = unwrappedOverlay.shape as? MKMultiPolyline {
|
||||
let renderer = MKMultiPolylineRenderer(multiPolyline: multiPolylineOverlay)
|
||||
renderer.fillColor = unwrappedOverlay.fillColor
|
||||
renderer.strokeColor = unwrappedOverlay.strokeColor
|
||||
renderer.lineWidth = unwrappedOverlay.lineWidth
|
||||
return renderer
|
||||
} else {
|
||||
return MKOverlayRenderer()
|
||||
}
|
||||
} else if let tileOverlay = overlay as? MKTileOverlay {
|
||||
return MKTileOverlayRenderer(tileOverlay: tileOverlay)
|
||||
} else {
|
||||
return MKOverlayRenderer()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// is supposed to be located in the folder with the map name
|
||||
public struct DefaultTile: Hashable {
|
||||
let tileName: String
|
||||
let tileType: String
|
||||
|
||||
public init(tileName: String, tileType: String) {
|
||||
self.tileName = tileName
|
||||
self.tileType = tileType
|
||||
}
|
||||
}
|
||||
|
||||
public struct CustomMapOverlay: Equatable, Hashable {
|
||||
let mapName: String
|
||||
let tileType: String
|
||||
var canReplaceMapContent: Bool
|
||||
var minimumZoomLevel: Int?
|
||||
var maximumZoomLevel: Int?
|
||||
let defaultTile: DefaultTile?
|
||||
|
||||
public init(
|
||||
mapName: String,
|
||||
tileType: String,
|
||||
canReplaceMapContent: Bool = true, // false for transparent tiles
|
||||
minimumZoomLevel: Int? = nil,
|
||||
maximumZoomLevel: Int? = nil,
|
||||
defaultTile: DefaultTile? = nil
|
||||
) {
|
||||
self.mapName = mapName
|
||||
self.tileType = tileType
|
||||
self.canReplaceMapContent = canReplaceMapContent
|
||||
self.minimumZoomLevel = minimumZoomLevel
|
||||
self.maximumZoomLevel = maximumZoomLevel
|
||||
self.defaultTile = defaultTile
|
||||
}
|
||||
|
||||
public init?(
|
||||
mapName: String?,
|
||||
tileType: String,
|
||||
canReplaceMapContent: Bool = true, // false for transparent tiles
|
||||
minimumZoomLevel: Int? = nil,
|
||||
maximumZoomLevel: Int? = nil,
|
||||
defaultTile: DefaultTile? = nil
|
||||
) {
|
||||
if (mapName == nil || mapName! == "") {
|
||||
return nil
|
||||
}
|
||||
self.mapName = mapName!
|
||||
self.tileType = tileType
|
||||
self.canReplaceMapContent = canReplaceMapContent
|
||||
self.minimumZoomLevel = minimumZoomLevel
|
||||
self.maximumZoomLevel = maximumZoomLevel
|
||||
self.defaultTile = defaultTile
|
||||
}
|
||||
}
|
||||
|
||||
public class CustomMapOverlaySource: MKTileOverlay {
|
||||
|
||||
// requires folder: tiles/{mapName}/z/y/y,{tileType}
|
||||
private var parent: MapViewSwiftUI
|
||||
private let mapName: String
|
||||
private let tileType: String
|
||||
private let defaultTile: DefaultTile?
|
||||
|
||||
public init(
|
||||
parent: MapViewSwiftUI,
|
||||
mapName: String,
|
||||
tileType: String,
|
||||
defaultTile: DefaultTile?
|
||||
) {
|
||||
self.parent = parent
|
||||
self.mapName = mapName
|
||||
self.tileType = tileType
|
||||
self.defaultTile = defaultTile
|
||||
super.init(urlTemplate: "")
|
||||
}
|
||||
|
||||
public override func url(forTilePath path: MKTileOverlayPath) -> URL {
|
||||
if let tileUrl = Bundle.main.url(
|
||||
forResource: "\(path.y)",
|
||||
withExtension: self.tileType,
|
||||
subdirectory: "tiles/\(self.mapName)/\(path.z)/\(path.x)",
|
||||
localization: nil
|
||||
) {
|
||||
return tileUrl
|
||||
} else if let defaultTile = self.defaultTile, let defaultTileUrl = Bundle.main.url(
|
||||
forResource: defaultTile.tileName,
|
||||
withExtension: defaultTile.tileType,
|
||||
subdirectory: "tiles/\(self.mapName)",
|
||||
localization: nil
|
||||
) {
|
||||
return defaultTileUrl
|
||||
} else {
|
||||
let urlstring = self.mapName+"\(path.z)/\(path.x)/\(path.y).png"
|
||||
return URL(string: urlstring)!
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public struct Overlay {
|
||||
|
||||
public static func == (lhs: MapViewSwiftUI.Overlay, rhs: MapViewSwiftUI.Overlay) -> Bool {
|
||||
// maybe to use in the future for comparison of full array
|
||||
lhs.shape.coordinate.latitude == rhs.shape.coordinate.latitude &&
|
||||
lhs.shape.coordinate.longitude == rhs.shape.coordinate.longitude &&
|
||||
lhs.fillColor == rhs.fillColor
|
||||
}
|
||||
|
||||
var shape: MKOverlay
|
||||
var fillColor: UIColor?
|
||||
var strokeColor: UIColor?
|
||||
var lineWidth: CGFloat
|
||||
|
||||
public init(
|
||||
shape: MKOverlay,
|
||||
fillColor: UIColor? = nil,
|
||||
strokeColor: UIColor? = nil,
|
||||
lineWidth: CGFloat = 0
|
||||
) {
|
||||
self.shape = shape
|
||||
self.fillColor = fillColor
|
||||
self.strokeColor = strokeColor
|
||||
self.lineWidth = lineWidth
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,60 +1,60 @@
|
|||
////
|
||||
//// PositionAnnotationView.swift
|
||||
//// MeshtasticApple
|
||||
////
|
||||
//// Created by Joshua Pirihi on 24/12/21.
|
||||
////
|
||||
//
|
||||
// PositionAnnotationView.swift
|
||||
// MeshtasticApple
|
||||
//import UIKit
|
||||
//import MapKit
|
||||
//import SwiftUI
|
||||
//
|
||||
// Created by Joshua Pirihi on 24/12/21.
|
||||
//// a simple circle annotation, with a string in it
|
||||
//class PositionAnnotation: NSObject, MKAnnotation {
|
||||
//
|
||||
|
||||
import UIKit
|
||||
import MapKit
|
||||
import SwiftUI
|
||||
|
||||
// a simple circle annotation, with a string in it
|
||||
class PositionAnnotation: NSObject, MKAnnotation {
|
||||
|
||||
// This property must be key-value observable, which the `@objc dynamic` attributes provide.
|
||||
@objc dynamic var coordinate = CLLocationCoordinate2D(latitude: 0, longitude: 0)
|
||||
|
||||
// Required if you set the annotation view's `canShowCallout` property to `true`
|
||||
// this string fills the callout label when you tap an annotation
|
||||
var title: String?
|
||||
|
||||
// the text to appear inside the little circle
|
||||
var shortName: String?
|
||||
|
||||
}
|
||||
|
||||
class PositionAnnotationView: MKAnnotationView {
|
||||
|
||||
private let annotationFrame = CGRect(x: 0, y: 0, width: 40, height: 40)
|
||||
private let label: UILabel
|
||||
|
||||
override init(annotation: MKAnnotation?, reuseIdentifier: String?) {
|
||||
self.label = UILabel(frame: annotationFrame.offsetBy(dx: 0, dy: 0))
|
||||
super.init(annotation: annotation, reuseIdentifier: reuseIdentifier)
|
||||
self.frame = annotationFrame
|
||||
self.label.font = UIFont.preferredFont(forTextStyle: .caption2)
|
||||
self.label.textColor = .white
|
||||
self.label.textAlignment = .center
|
||||
self.backgroundColor = .clear
|
||||
self.addSubview(label)
|
||||
}
|
||||
|
||||
required init?(coder aDecoder: NSCoder) {
|
||||
fatalError("init(coder:) not implemented!")
|
||||
}
|
||||
|
||||
public var name: String = "" {
|
||||
didSet {
|
||||
self.label.text = name
|
||||
}
|
||||
}
|
||||
|
||||
override func draw(_ rect: CGRect) {
|
||||
guard let context = UIGraphicsGetCurrentContext() else { return }
|
||||
|
||||
let circleRect = CGRect(x: 1, y: 1, width: 38, height: 38)
|
||||
context.setFillColor(Color.accentColor.cgColor ?? CGColor(red: 0, green: 0.5, blue: 1.0, alpha: 1.0))
|
||||
context.fillEllipse(in: circleRect)
|
||||
}
|
||||
}
|
||||
// // This property must be key-value observable, which the `@objc dynamic` attributes provide.
|
||||
// @objc dynamic var coordinate = CLLocationCoordinate2D(latitude: 0, longitude: 0)
|
||||
//
|
||||
// // Required if you set the annotation view's `canShowCallout` property to `true`
|
||||
// // this string fills the callout label when you tap an annotation
|
||||
// var title: String?
|
||||
//
|
||||
// // the text to appear inside the little circle
|
||||
// var shortName: String?
|
||||
//
|
||||
//}
|
||||
//
|
||||
//class PositionAnnotationView: MKAnnotationView {
|
||||
//
|
||||
// private let annotationFrame = CGRect(x: 0, y: 0, width: 40, height: 40)
|
||||
// private let label: UILabel
|
||||
//
|
||||
// override init(annotation: MKAnnotation?, reuseIdentifier: String?) {
|
||||
// self.label = UILabel(frame: annotationFrame.offsetBy(dx: 0, dy: 0))
|
||||
// super.init(annotation: annotation, reuseIdentifier: reuseIdentifier)
|
||||
// self.frame = annotationFrame
|
||||
// self.label.font = UIFont.preferredFont(forTextStyle: .caption2)
|
||||
// self.label.textColor = .white
|
||||
// self.label.textAlignment = .center
|
||||
// self.backgroundColor = .clear
|
||||
// self.addSubview(label)
|
||||
// }
|
||||
//
|
||||
// required init?(coder aDecoder: NSCoder) {
|
||||
// fatalError("init(coder:) not implemented!")
|
||||
// }
|
||||
//
|
||||
// public var name: String = "" {
|
||||
// didSet {
|
||||
// self.label.text = name
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// override func draw(_ rect: CGRect) {
|
||||
// guard let context = UIGraphicsGetCurrentContext() else { return }
|
||||
//
|
||||
// let circleRect = CGRect(x: 1, y: 1, width: 38, height: 38)
|
||||
// context.setFillColor(Color.accentColor.cgColor ?? CGColor(red: 0, green: 0.5, blue: 1.0, alpha: 1.0))
|
||||
// context.fillEllipse(in: circleRect)
|
||||
// }
|
||||
//}
|
||||
|
|
|
|||
|
|
@ -1,173 +0,0 @@
|
|||
//
|
||||
// MapView.swift
|
||||
// MeshtasticApple
|
||||
//
|
||||
// Created by Joshua Pirihi on 22/12/21.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import UIKit
|
||||
import MapKit
|
||||
import SwiftUI
|
||||
import CoreData
|
||||
#if false
|
||||
// wrap a MKMapView into something we can use in SwiftUI
|
||||
struct MapView: UIViewRepresentable {
|
||||
|
||||
var nodes: FetchedResults<NodeInfoEntity>
|
||||
|
||||
var mapViewDelegate = MapViewDelegate()
|
||||
|
||||
// observe changes to the key in UserDefaults
|
||||
@AppStorage("meshMapType") var type: String = "hybrid"
|
||||
|
||||
func makeUIView(context: Context) -> MKMapView {
|
||||
|
||||
let map = MKMapView(frame: .zero)
|
||||
|
||||
map.userTrackingMode = .follow
|
||||
|
||||
let region = MKCoordinateRegion( center: map.centerCoordinate, latitudinalMeters: CLLocationDistance(exactly: 500)!, longitudinalMeters: CLLocationDistance(exactly: 500)!)
|
||||
map.setRegion(map.regionThatFits(region), animated: false)
|
||||
|
||||
//self.updateMapType(map)
|
||||
self.showNodePositions(to: map)
|
||||
self.moveToMeshRegion(in: map)
|
||||
|
||||
map.register(PositionAnnotationView.self, forAnnotationViewWithReuseIdentifier: NSStringFromClass(PositionAnnotationView.self))
|
||||
|
||||
let overlay = MKTileOverlay(urlTemplate: //"http://tiles-a.data-cdn.linz.govt.nz/services;key=7fa19132d53240708c4ff436df5b9800/tiles/v4/layer=50767/EPSG:3857/{z}/{x}/{y}.png")
|
||||
"http://10.147.253.250:5050/local/map/{z}/{x}/{y}.png")
|
||||
overlay.canReplaceMapContent = true
|
||||
self.mapViewDelegate.renderer = MKTileOverlayRenderer(tileOverlay: overlay)
|
||||
map.addOverlay(overlay)
|
||||
|
||||
return map
|
||||
}
|
||||
|
||||
func updateUIView(_ view: MKMapView, context: Context) {
|
||||
view.delegate = mapViewDelegate // (1) This should be set in makeUIView, but it is getting reset to `nil`
|
||||
view.translatesAutoresizingMaskIntoConstraints = false // (2) In the absence of this, we get constraints error on rotation; and again, it seems one should do this in makeUIView, but has to be here
|
||||
|
||||
self.updateMapType(view)
|
||||
|
||||
self.showNodePositions(to: view)
|
||||
|
||||
//if (self.needToMoveToMeshRegion) {
|
||||
// self.moveToMeshRegion(in: view)
|
||||
// self.needToMoveToMeshRegion = false
|
||||
//}
|
||||
}
|
||||
|
||||
func moveToMeshRegion(in mapView: MKMapView) {
|
||||
//go through the annotations and create a bounding box that encloses them
|
||||
|
||||
var minLat: CLLocationDegrees = 90.0
|
||||
var maxLat: CLLocationDegrees = -90.0
|
||||
var minLon: CLLocationDegrees = 180.0
|
||||
var maxLon: CLLocationDegrees = -180.0
|
||||
|
||||
for annotation in mapView.annotations {
|
||||
if annotation.isKind(of: PositionAnnotation.self) {
|
||||
minLat = min(minLat, annotation.coordinate.latitude)
|
||||
maxLat = max(maxLat, annotation.coordinate.latitude)
|
||||
minLon = min(minLon, annotation.coordinate.longitude)
|
||||
maxLon = max(maxLon, annotation.coordinate.longitude)
|
||||
}
|
||||
}
|
||||
|
||||
//check if the mesh region looks sensible before we move to it. Otherwise we won't move the map (leave it at the current location)
|
||||
if maxLat < minLat || (maxLat-minLat) > 5 || maxLon < minLon || (maxLon-minLon) > 5 {
|
||||
return
|
||||
}
|
||||
|
||||
let centerCoord = CLLocationCoordinate2D(latitude: (minLat+maxLat)/2, longitude: (minLon+maxLon)/2)
|
||||
|
||||
let span = MKCoordinateSpan(latitudeDelta: (maxLat-minLat)*1.5, longitudeDelta: (maxLon-minLon)*1.5)
|
||||
|
||||
let region = mapView.regionThatFits(MKCoordinateRegion(center: centerCoord, span: span))
|
||||
|
||||
mapView.setRegion(region, animated: true)
|
||||
|
||||
|
||||
}
|
||||
|
||||
func updateMapType(_ map: MKMapView) {
|
||||
|
||||
switch self.type {
|
||||
case "satellite":
|
||||
map.mapType = .satellite
|
||||
break
|
||||
case "standard":
|
||||
map.mapType = .standard
|
||||
break
|
||||
case "hybrid":
|
||||
map.mapType = .hybrid
|
||||
break
|
||||
default:
|
||||
map.mapType = .hybrid
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private extension MapView {
|
||||
|
||||
func showNodePositions(to view: MKMapView) {
|
||||
|
||||
// clear any existing annotations
|
||||
if !view.annotations.isEmpty {
|
||||
view.removeAnnotations(view.annotations)
|
||||
}
|
||||
|
||||
for node in self.nodes {
|
||||
// try and get the last position
|
||||
if (node.positions?.count ?? 0) > 0 && (node.positions!.lastObject as! PositionEntity).coordinate != nil {
|
||||
let annotation = PositionAnnotation()
|
||||
annotation.coordinate = (node.positions!.lastObject as! PositionEntity).coordinate!
|
||||
annotation.title = node.user?.longName ?? NSLocalizedString("unknown", comment: "Unknown")
|
||||
annotation.shortName = node.user?.shortName?.uppercased() ?? "???"
|
||||
|
||||
view.addAnnotation(annotation)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class MapViewDelegate: NSObject, MKMapViewDelegate {
|
||||
|
||||
var renderer: MKTileOverlayRenderer?
|
||||
|
||||
func mapView(_ mapView: MKMapView, viewFor annotation: MKAnnotation) -> MKAnnotationView? {
|
||||
|
||||
guard !annotation.isKind(of: MKUserLocation.self) else {
|
||||
// Make a fast exit if the annotation is the `MKUserLocation`, as it's not an annotation view we wish to customize.
|
||||
return nil
|
||||
}
|
||||
|
||||
var annotationView: MKAnnotationView?
|
||||
|
||||
if let annotation = annotation as? PositionAnnotation {
|
||||
annotationView = self.setupPositionAnnotationView(for: annotation, on: mapView)
|
||||
}
|
||||
|
||||
return annotationView
|
||||
}
|
||||
|
||||
func mapView(_ mapView: MKMapView, rendererFor overlay: MKOverlay) -> MKOverlayRenderer {
|
||||
return self.renderer!
|
||||
|
||||
}
|
||||
|
||||
private func setupPositionAnnotationView(for annotation: PositionAnnotation, on mapView: MKMapView) -> PositionAnnotationView {
|
||||
let identifier = NSStringFromClass(PositionAnnotationView.self)
|
||||
|
||||
let annotationView = mapView.dequeueReusableAnnotationView(withIdentifier: identifier) as? PositionAnnotationView ?? PositionAnnotationView()
|
||||
|
||||
annotationView.name = annotation.shortName ?? "???"
|
||||
|
||||
annotationView.canShowCallout = true
|
||||
|
||||
return annotationView
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
|
@ -1,767 +0,0 @@
|
|||
//
|
||||
// MapView.swift
|
||||
// MapViewTest
|
||||
//
|
||||
// Created by Cem Yilmaz on 05.07.21.
|
||||
//
|
||||
import SwiftUI
|
||||
import MapKit
|
||||
import CoreData
|
||||
|
||||
#if canImport(MapKit) && canImport(UIKit)
|
||||
public struct MapView: UIViewRepresentable {
|
||||
|
||||
@Environment(\.managedObjectContext) var context
|
||||
|
||||
//var context: NSManagedObjectContext?
|
||||
|
||||
//@Binding private var region: MKCoordinateRegion
|
||||
|
||||
//make this view dependent on the UserDefault that is updated when importing a new map file
|
||||
@AppStorage("lastUpdatedLocalMapFile") private var lastUpdatedLocalMapFile = 0
|
||||
@State private var loadedLastUpdatedLocalMapFile = 0
|
||||
|
||||
private var customMapOverlay: CustomMapOverlay?
|
||||
@State private var presentCustomMapOverlayHash: CustomMapOverlay?
|
||||
|
||||
private var mapType: MKMapType
|
||||
|
||||
private var showZoomScale: Bool
|
||||
private var zoomEnabled: Bool
|
||||
private var zoomRange: (minHeight: CLLocationDistance?, maxHeight: CLLocationDistance?)
|
||||
|
||||
private var scrollEnabled: Bool
|
||||
private var scrollBoundaries: MKCoordinateRegion?
|
||||
|
||||
private var rotationEnabled: Bool
|
||||
private var showCompassWhenRotated: Bool
|
||||
|
||||
private var showUserLocation: Bool
|
||||
private var userTrackingMode: MKUserTrackingMode
|
||||
@Binding private var userLocation: CLLocationCoordinate2D?
|
||||
|
||||
//private var annotations: [MKPointAnnotation]
|
||||
|
||||
private var overlays: [Overlay]
|
||||
|
||||
//@FetchRequest(sortDescriptors: [NSSortDescriptor(key: "lastHeard", ascending: false)], animation: .default)
|
||||
// private var locationNodes: FetchedResults<NodeInfoEntity>
|
||||
|
||||
@FetchRequest(sortDescriptors: [NSSortDescriptor(key: "time", ascending: false)], animation: .default)
|
||||
private var positions: FetchedResults<PositionEntity>
|
||||
|
||||
//@State private var locationNodes: [NodeInfoEntity]
|
||||
|
||||
public init(
|
||||
//region: Binding<MKCoordinateRegion> = .constant(MKCoordinateRegion()),
|
||||
customMapOverlay: CustomMapOverlay? = nil,
|
||||
//mapType: MKMapType = MKMapType.standard,
|
||||
mapType: String = "hybrid",
|
||||
zoomEnabled: Bool = true,
|
||||
showZoomScale: Bool = false,
|
||||
zoomRange: (minHeight: CLLocationDistance?, maxHeight: CLLocationDistance?) = (nil, nil),
|
||||
scrollEnabled: Bool = true,
|
||||
scrollBoundaries: MKCoordinateRegion? = nil,
|
||||
rotationEnabled: Bool = true,
|
||||
showCompassWhenRotated: Bool = true,
|
||||
showUserLocation: Bool = true,
|
||||
userTrackingMode: MKUserTrackingMode = MKUserTrackingMode.none,
|
||||
userLocation: Binding<CLLocationCoordinate2D?> = .constant(nil),
|
||||
//annotations: [MKPointAnnotation] = [],
|
||||
//locationNodes: [NodeInfoEntity] = [],
|
||||
overlays: [Overlay] = []
|
||||
//context: NSManagedObjectContext? = nil
|
||||
) {
|
||||
//self._region = region
|
||||
|
||||
self.customMapOverlay = customMapOverlay
|
||||
|
||||
switch mapType {
|
||||
case "satellite":
|
||||
self.mapType = .satellite
|
||||
break
|
||||
case "standard":
|
||||
self.mapType = .standard
|
||||
break
|
||||
case "hybrid":
|
||||
self.mapType = .hybrid
|
||||
break
|
||||
default:
|
||||
self.mapType = .hybrid
|
||||
}
|
||||
//self.mapType = mapType
|
||||
|
||||
self.showZoomScale = showZoomScale
|
||||
self.zoomEnabled = zoomEnabled
|
||||
self.zoomRange = zoomRange
|
||||
|
||||
self.scrollEnabled = scrollEnabled
|
||||
self.scrollBoundaries = scrollBoundaries
|
||||
|
||||
self.rotationEnabled = rotationEnabled
|
||||
self.showCompassWhenRotated = showCompassWhenRotated
|
||||
|
||||
self.showUserLocation = showUserLocation
|
||||
self.userTrackingMode = userTrackingMode
|
||||
self._userLocation = userLocation
|
||||
|
||||
//self.annotations = annotations
|
||||
|
||||
//self.locationNodes = locationNodes
|
||||
|
||||
self.overlays = overlays
|
||||
|
||||
}
|
||||
|
||||
public func makeUIView(context: Context) -> MKMapView {
|
||||
let mapView = MKMapView()
|
||||
mapView.delegate = context.coordinator
|
||||
mapView.register(PositionAnnotationView.self, forAnnotationViewWithReuseIdentifier: NSStringFromClass(PositionAnnotationView.self))
|
||||
|
||||
return mapView
|
||||
}
|
||||
|
||||
|
||||
public func updateUIView(_ mapView: MKMapView, context: Context) {
|
||||
|
||||
//if self.userTrackingMode == MKUserTrackingMode.none && (mapView.region.center.latitude != self.region.center.latitude || mapView.region.center.longitude != self.region.center.longitude) {
|
||||
//mapView.region = self.region
|
||||
//}
|
||||
|
||||
if self.customMapOverlay != self.presentCustomMapOverlayHash || self.loadedLastUpdatedLocalMapFile != self.lastUpdatedLocalMapFile {
|
||||
mapView.removeOverlays(mapView.overlays)
|
||||
if let customMapOverlay = self.customMapOverlay {
|
||||
|
||||
let fileManager = FileManager.default
|
||||
let documentsDirectory = fileManager.urls(for: .documentDirectory, in: .userDomainMask).first!
|
||||
let tilePath = documentsDirectory.appendingPathComponent("offline_map.mbtiles", isDirectory: false).path
|
||||
if fileManager.fileExists(atPath: tilePath) {
|
||||
//if let tilePath = Bundle.main.path(forResource: "offline_map", ofType: "mbtiles") {
|
||||
|
||||
print("Loading local map file")
|
||||
|
||||
if let overlay = LocalMBTileOverlay(mbTilePath: tilePath) {
|
||||
|
||||
overlay.canReplaceMapContent = false//customMapOverlay.canReplaceMapContent
|
||||
|
||||
mapView.addOverlay(overlay)
|
||||
}
|
||||
} else {
|
||||
print("Couldn't find a local map file to load")
|
||||
}
|
||||
}
|
||||
DispatchQueue.main.async {
|
||||
self.presentCustomMapOverlayHash = self.customMapOverlay
|
||||
self.loadedLastUpdatedLocalMapFile = self.lastUpdatedLocalMapFile
|
||||
}
|
||||
}
|
||||
|
||||
/*if mapView.overlays.count != (self.overlays.count + (self.customMapOverlay == nil ? 0 : 1)) {
|
||||
context.coordinator.overlays = self.overlays
|
||||
mapView.overlays.forEach { overlay in
|
||||
if !(overlay is MKTileOverlay) {
|
||||
mapView.removeOverlay(overlay)
|
||||
}
|
||||
}
|
||||
mapView.addOverlays(self.overlays.map { overlay in overlay.shape })
|
||||
}*/
|
||||
|
||||
if mapView.mapType != self.mapType {
|
||||
mapView.mapType = self.mapType
|
||||
}
|
||||
|
||||
mapView.showsScale = self.zoomEnabled ? self.showZoomScale : false
|
||||
|
||||
if mapView.isZoomEnabled != self.zoomEnabled {
|
||||
mapView.isZoomEnabled = self.zoomEnabled
|
||||
}
|
||||
|
||||
if mapView.cameraZoomRange.minCenterCoordinateDistance != self.zoomRange.minHeight ?? 0 ||
|
||||
mapView.cameraZoomRange.maxCenterCoordinateDistance != self.zoomRange.maxHeight ?? .infinity {
|
||||
mapView.cameraZoomRange = MKMapView.CameraZoomRange(
|
||||
minCenterCoordinateDistance: self.zoomRange.minHeight ?? 0,
|
||||
maxCenterCoordinateDistance: self.zoomRange.maxHeight ?? .infinity
|
||||
)
|
||||
}
|
||||
|
||||
mapView.isScrollEnabled = self.userTrackingMode == MKUserTrackingMode.none ? self.scrollEnabled : false
|
||||
|
||||
if let scrollBoundary = self.scrollBoundaries, (mapView.cameraBoundary?.region.center.latitude != scrollBoundary.center.latitude || mapView.cameraBoundary?.region.center.longitude != scrollBoundary.center.longitude || mapView.cameraBoundary?.region.span.latitudeDelta != scrollBoundary.span.latitudeDelta || mapView.cameraBoundary?.region.span.longitudeDelta != scrollBoundary.span.longitudeDelta) {
|
||||
mapView.cameraBoundary = MKMapView.CameraBoundary(coordinateRegion: scrollBoundary)
|
||||
} else if self.scrollBoundaries == nil && mapView.cameraBoundary != nil {
|
||||
mapView.cameraBoundary = nil
|
||||
}
|
||||
|
||||
mapView.isRotateEnabled = self.userTrackingMode != .followWithHeading ? self.rotationEnabled : false
|
||||
mapView.showsCompass = self.userTrackingMode != .followWithHeading ? self.showCompassWhenRotated : false
|
||||
|
||||
if mapView.showsUserLocation != self.showUserLocation {
|
||||
mapView.showsUserLocation = self.showUserLocation
|
||||
}
|
||||
|
||||
if mapView.userTrackingMode != self.userTrackingMode {
|
||||
mapView.userTrackingMode = self.userTrackingMode
|
||||
}
|
||||
|
||||
//if mapView.annotations.filter({ annotation in !(annotation is MKUserLocation) }).count != self.annotations.count {
|
||||
// mapView.removeAnnotations(mapView.annotations)
|
||||
// mapView.addAnnotations(self.annotations)
|
||||
//}
|
||||
|
||||
// clear any existing annotations
|
||||
var shouldMoveRegion = false
|
||||
if !mapView.annotations.isEmpty {
|
||||
mapView.removeAnnotations(mapView.annotations)
|
||||
} else {
|
||||
shouldMoveRegion = true
|
||||
}
|
||||
|
||||
var displayedNodes: [Int64] = []
|
||||
for position in self.positions {
|
||||
if position.nodePosition == nil || displayedNodes.contains(position.nodePosition!.num) || position.coordinate == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
let annotation = PositionAnnotation()
|
||||
annotation.coordinate = position.coordinate!
|
||||
annotation.title = position.nodePosition!.user?.longName ?? NSLocalizedString("unknown", comment: "Unknown")
|
||||
annotation.shortName = position.nodePosition!.user?.shortName?.uppercased() ?? "???"
|
||||
|
||||
mapView.addAnnotation(annotation)
|
||||
|
||||
displayedNodes.append(position.nodePosition!.num)
|
||||
}
|
||||
|
||||
if shouldMoveRegion {
|
||||
self.moveToMeshRegion(mapView)
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
func moveToMeshRegion(_ mapView: MKMapView) {
|
||||
//go through the annotations and create a bounding box that encloses them
|
||||
|
||||
var minLat: CLLocationDegrees = 90.0
|
||||
var maxLat: CLLocationDegrees = -90.0
|
||||
var minLon: CLLocationDegrees = 180.0
|
||||
var maxLon: CLLocationDegrees = -180.0
|
||||
|
||||
for annotation in mapView.annotations {
|
||||
if annotation.isKind(of: PositionAnnotation.self) {
|
||||
minLat = min(minLat, annotation.coordinate.latitude)
|
||||
maxLat = max(maxLat, annotation.coordinate.latitude)
|
||||
minLon = min(minLon, annotation.coordinate.longitude)
|
||||
maxLon = max(maxLon, annotation.coordinate.longitude)
|
||||
}
|
||||
}
|
||||
|
||||
//check if the mesh region looks sensible before we move to it. Otherwise we won't move the map (leave it at the current location)
|
||||
if maxLat < minLat || (maxLat-minLat) > 5 || maxLon < minLon || (maxLon-minLon) > 5 {
|
||||
return
|
||||
} else if minLat == maxLat && minLon == maxLon {
|
||||
//then we are focussed on a single point (probably because there is only one node with a position)
|
||||
//widen that out a little (don't zoom way in to that point)
|
||||
|
||||
//0.001 degrees latitude is about 100m
|
||||
//the mapView.regionThatFits call below will expand this out to a rectangle
|
||||
minLat = minLat - 0.001
|
||||
maxLat = maxLat + 0.001
|
||||
}
|
||||
|
||||
let centerCoord = CLLocationCoordinate2D(latitude: (minLat+maxLat)/2, longitude: (minLon+maxLon)/2)
|
||||
|
||||
let span = MKCoordinateSpan(latitudeDelta: (maxLat-minLat)*1.5, longitudeDelta: (maxLon-minLon)*1.5)
|
||||
|
||||
let region = mapView.regionThatFits(MKCoordinateRegion(center: centerCoord, span: span))
|
||||
|
||||
mapView.setRegion(region, animated: true)
|
||||
|
||||
|
||||
}
|
||||
|
||||
public func makeCoordinator() -> Coordinator {
|
||||
Coordinator(parent: self)
|
||||
}
|
||||
|
||||
public class Coordinator: NSObject, MKMapViewDelegate {
|
||||
|
||||
private var parent: MapView
|
||||
public var overlays: [Overlay] = []
|
||||
|
||||
init(parent: MapView) {
|
||||
self.parent = parent
|
||||
}
|
||||
|
||||
/*public func mapViewDidChangeVisibleRegion(_ mapView: MKMapView) {
|
||||
DispatchQueue.main.async {
|
||||
self.parent.region = mapView.region
|
||||
}
|
||||
}*/
|
||||
|
||||
public func mapView(_ mapView: MKMapView, viewFor annotation: MKAnnotation) -> MKAnnotationView? {
|
||||
|
||||
guard !annotation.isKind(of: MKUserLocation.self) else {
|
||||
// Make a fast exit if the annotation is the `MKUserLocation`, as it's not an annotation view we wish to customize.
|
||||
return nil
|
||||
}
|
||||
//var annotationView: MKAnnotationView?
|
||||
|
||||
if let annotation = annotation as? PositionAnnotation {
|
||||
|
||||
let annotationView = PositionAnnotationView(annotation: annotation, reuseIdentifier: "PositionAnnotation")
|
||||
annotationView.name = annotation.shortName ?? "????"
|
||||
annotationView.canShowCallout = true
|
||||
|
||||
return annotationView
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
public func mapView(_ mapView: MKMapView, rendererFor overlay: MKOverlay) -> MKOverlayRenderer {
|
||||
|
||||
if let index = self.overlays.firstIndex(where: { overlay_ in overlay_.shape.hash == overlay.hash }) {
|
||||
|
||||
let unwrappedOverlay = self.overlays[index]
|
||||
|
||||
if let circleOverlay = unwrappedOverlay.shape as? MKCircle {
|
||||
|
||||
let renderer = MKCircleRenderer(circle: circleOverlay)
|
||||
renderer.fillColor = unwrappedOverlay.fillColor
|
||||
renderer.strokeColor = unwrappedOverlay.strokeColor
|
||||
renderer.lineWidth = unwrappedOverlay.lineWidth
|
||||
return renderer
|
||||
|
||||
} else if let polygonOverlay = unwrappedOverlay.shape as? MKPolygon {
|
||||
|
||||
let renderer = MKPolygonRenderer(polygon: polygonOverlay)
|
||||
renderer.fillColor = unwrappedOverlay.fillColor
|
||||
renderer.strokeColor = unwrappedOverlay.strokeColor
|
||||
renderer.lineWidth = unwrappedOverlay.lineWidth
|
||||
return renderer
|
||||
|
||||
} else if let multiPolygonOverlay = unwrappedOverlay.shape as? MKMultiPolygon {
|
||||
|
||||
let renderer = MKMultiPolygonRenderer(multiPolygon: multiPolygonOverlay)
|
||||
renderer.fillColor = unwrappedOverlay.fillColor
|
||||
renderer.strokeColor = unwrappedOverlay.strokeColor
|
||||
renderer.lineWidth = unwrappedOverlay.lineWidth
|
||||
return renderer
|
||||
|
||||
} else if let polyLineOverlay = unwrappedOverlay.shape as? MKPolyline {
|
||||
|
||||
let renderer = MKPolylineRenderer(polyline: polyLineOverlay)
|
||||
renderer.fillColor = unwrappedOverlay.fillColor
|
||||
renderer.strokeColor = unwrappedOverlay.strokeColor
|
||||
renderer.lineWidth = unwrappedOverlay.lineWidth
|
||||
return renderer
|
||||
|
||||
} else if let multiPolylineOverlay = unwrappedOverlay.shape as? MKMultiPolyline {
|
||||
|
||||
let renderer = MKMultiPolylineRenderer(multiPolyline: multiPolylineOverlay)
|
||||
renderer.fillColor = unwrappedOverlay.fillColor
|
||||
renderer.strokeColor = unwrappedOverlay.strokeColor
|
||||
renderer.lineWidth = unwrappedOverlay.lineWidth
|
||||
return renderer
|
||||
|
||||
} else {
|
||||
|
||||
return MKOverlayRenderer()
|
||||
|
||||
}
|
||||
|
||||
} else if let tileOverlay = overlay as? MKTileOverlay {
|
||||
|
||||
return MKTileOverlayRenderer(tileOverlay: tileOverlay)
|
||||
|
||||
} else {
|
||||
|
||||
return MKOverlayRenderer()
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
}
|
||||
|
||||
/// is supposed to be located in the folder with the map name
|
||||
public struct DefaultTile: Hashable {
|
||||
let tileName: String
|
||||
let tileType: String
|
||||
|
||||
public init(tileName: String, tileType: String) {
|
||||
self.tileName = tileName
|
||||
self.tileType = tileType
|
||||
}
|
||||
}
|
||||
|
||||
public struct CustomMapOverlay: Equatable, Hashable {
|
||||
let mapName: String
|
||||
let tileType: String
|
||||
var canReplaceMapContent: Bool
|
||||
var minimumZoomLevel: Int?
|
||||
var maximumZoomLevel: Int?
|
||||
let defaultTile: DefaultTile?
|
||||
|
||||
public init(
|
||||
mapName: String,
|
||||
tileType: String,
|
||||
canReplaceMapContent: Bool = true, // false for transparent tiles
|
||||
minimumZoomLevel: Int? = nil,
|
||||
maximumZoomLevel: Int? = nil,
|
||||
defaultTile: DefaultTile? = nil
|
||||
) {
|
||||
|
||||
self.mapName = mapName
|
||||
self.tileType = tileType
|
||||
self.canReplaceMapContent = canReplaceMapContent
|
||||
self.minimumZoomLevel = minimumZoomLevel
|
||||
self.maximumZoomLevel = maximumZoomLevel
|
||||
self.defaultTile = defaultTile
|
||||
}
|
||||
|
||||
public init?(
|
||||
mapName: String?,
|
||||
tileType: String,
|
||||
canReplaceMapContent: Bool = true, // false for transparent tiles
|
||||
minimumZoomLevel: Int? = nil,
|
||||
maximumZoomLevel: Int? = nil,
|
||||
defaultTile: DefaultTile? = nil
|
||||
) {
|
||||
if (mapName == nil || mapName! == "") {
|
||||
return nil
|
||||
}
|
||||
self.mapName = mapName!
|
||||
self.tileType = tileType
|
||||
self.canReplaceMapContent = canReplaceMapContent
|
||||
self.minimumZoomLevel = minimumZoomLevel
|
||||
self.maximumZoomLevel = maximumZoomLevel
|
||||
self.defaultTile = defaultTile
|
||||
}
|
||||
}
|
||||
|
||||
public class CustomMapOverlaySource: MKTileOverlay {
|
||||
|
||||
// requires folder: tiles/{mapName}/z/y/y,{tileType}
|
||||
private var parent: MapView
|
||||
private let mapName: String
|
||||
private let tileType: String
|
||||
private let defaultTile: DefaultTile?
|
||||
|
||||
public init(
|
||||
parent: MapView,
|
||||
mapName: String,
|
||||
tileType: String,
|
||||
defaultTile: DefaultTile?
|
||||
) {
|
||||
self.parent = parent
|
||||
self.mapName = mapName
|
||||
self.tileType = tileType
|
||||
self.defaultTile = defaultTile
|
||||
super.init(urlTemplate: "")
|
||||
}
|
||||
|
||||
public override func url(forTilePath path: MKTileOverlayPath) -> URL {
|
||||
if let tileUrl = Bundle.main.url(
|
||||
forResource: "\(path.y)",
|
||||
withExtension: self.tileType,
|
||||
subdirectory: "tiles/\(self.mapName)/\(path.z)/\(path.x)",
|
||||
localization: nil
|
||||
) {
|
||||
return tileUrl
|
||||
} else if let defaultTile = self.defaultTile, let defaultTileUrl = Bundle.main.url(
|
||||
forResource: defaultTile.tileName,
|
||||
withExtension: defaultTile.tileType,
|
||||
subdirectory: "tiles/\(self.mapName)",
|
||||
localization: nil
|
||||
) {
|
||||
return defaultTileUrl
|
||||
} else {
|
||||
let urlstring = self.mapName+"\(path.z)/\(path.x)/\(path.y).png"
|
||||
return URL(string: urlstring)!
|
||||
// Bundle.main.url(forResource: "surrounding", withExtension: "png", subdirectory: "tiles")!
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public struct Overlay {
|
||||
|
||||
public static func == (lhs: MapView.Overlay, rhs: MapView.Overlay) -> Bool {
|
||||
// maybe to use in the future for comparison of full array
|
||||
lhs.shape.coordinate.latitude == rhs.shape.coordinate.latitude &&
|
||||
lhs.shape.coordinate.longitude == rhs.shape.coordinate.longitude &&
|
||||
lhs.fillColor == rhs.fillColor
|
||||
}
|
||||
|
||||
var shape: MKOverlay
|
||||
var fillColor: UIColor?
|
||||
var strokeColor: UIColor?
|
||||
var lineWidth: CGFloat
|
||||
|
||||
public init(
|
||||
shape: MKOverlay,
|
||||
fillColor: UIColor? = nil,
|
||||
strokeColor: UIColor? = nil,
|
||||
lineWidth: CGFloat = 0
|
||||
) {
|
||||
self.shape = shape
|
||||
self.fillColor = fillColor
|
||||
self.strokeColor = strokeColor
|
||||
self.lineWidth = lineWidth
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// MARK: End of implementation
|
||||
// MARK: Demonstration
|
||||
/*
|
||||
public struct MapViewDemo: View {
|
||||
|
||||
@State private var locationManager: CLLocationManager
|
||||
|
||||
@State private var mapRegion: MKCoordinateRegion = MKCoordinateRegion(
|
||||
center: CLLocationCoordinate2D(
|
||||
latitude: -38.758247,
|
||||
longitude: 175.360208
|
||||
),
|
||||
span: MKCoordinateSpan(
|
||||
latitudeDelta: 0.01,
|
||||
longitudeDelta: 0.01
|
||||
)
|
||||
)
|
||||
|
||||
@State private var customMapOverlay: MapView.CustomMapOverlay?
|
||||
|
||||
@State private var mapType: MKMapType = MKMapType.standard
|
||||
|
||||
@State private var zoomEnabled: Bool = true
|
||||
@State private var showZoomScale: Bool = true
|
||||
@State private var useMinZoomBoundary: Bool = false
|
||||
@State private var minZoom: Double = 0
|
||||
@State private var useMaxZoomBoundary: Bool = false
|
||||
@State private var maxZoom: Double = 3000000
|
||||
|
||||
@State private var scrollEnabled: Bool = true
|
||||
@State private var useScrollBoundaries: Bool = false
|
||||
@State private var scrollBoundaries: MKCoordinateRegion = MKCoordinateRegion()
|
||||
|
||||
@State private var rotationEnabled: Bool = true
|
||||
@State private var showCompassWhenRotated: Bool = true
|
||||
|
||||
@State private var showUserLocation: Bool = true
|
||||
@State private var userTrackingMode: MKUserTrackingMode = MKUserTrackingMode.none
|
||||
@State private var userLocation: CLLocationCoordinate2D?
|
||||
|
||||
@State private var showAnnotations: Bool = true
|
||||
@State private var annotations: [MKPointAnnotation] = []
|
||||
|
||||
@State private var showOverlays: Bool = true
|
||||
@State private var overlays: [MapView.Overlay] = []
|
||||
|
||||
@State private var showMapCenter: Bool = false
|
||||
|
||||
public init() {
|
||||
self.locationManager = CLLocationManager()
|
||||
self.locationManager.requestWhenInUseAuthorization()
|
||||
}
|
||||
|
||||
public var body: some View {
|
||||
|
||||
NavigationView {
|
||||
|
||||
List {
|
||||
|
||||
Section(header: Text("Scroll")) {
|
||||
Toggle("Scroll enabled", isOn: self.$scrollEnabled)
|
||||
Toggle("Use scroll boundaries", isOn: self.$useScrollBoundaries)
|
||||
.onChange(of: self.useScrollBoundaries) { newValue in
|
||||
if newValue {
|
||||
self.scrollBoundaries = MKCoordinateRegion(center: self.mapRegion.center, span: MKCoordinateSpan())
|
||||
}
|
||||
}
|
||||
if self.useScrollBoundaries {
|
||||
VStack(alignment: .leading) {
|
||||
Text(String(format: "Vertical distance to center: %.2f m", self.scrollBoundaries.span.latitudeDelta * 10609))
|
||||
Slider(value: self.$scrollBoundaries.span.latitudeDelta, in: 0...(300/10609))
|
||||
}
|
||||
VStack(alignment: .leading) {
|
||||
Text(String(format: "Horizontal distance to center: %.2f m", self.self.scrollBoundaries.span.longitudeDelta * 10609))
|
||||
Slider(value: self.$scrollBoundaries.span.longitudeDelta, in: 0...(300/10609))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Section(header: Text("Zoom")) {
|
||||
Toggle("Zoom enabled", isOn: self.$zoomEnabled)
|
||||
Toggle("Show zoom scale", isOn: self.$showZoomScale)
|
||||
Toggle("Use minimum zoom boundary", isOn: self.$useMinZoomBoundary)
|
||||
if self.useMinZoomBoundary {
|
||||
VStack(alignment: .leading) {
|
||||
Text(String(format: "Minimum Height: %.2f m", self.minZoom))
|
||||
Slider(value: self.$minZoom, in: 0...(self.useMaxZoomBoundary ? self.maxZoom : 3000000), step: 10)
|
||||
}
|
||||
}
|
||||
Toggle("Use maximum zoom boundary", isOn: self.$useMaxZoomBoundary)
|
||||
if self.useMaxZoomBoundary {
|
||||
VStack(alignment: .leading) {
|
||||
Text(String(format: "Maximum Height: %.2f m", self.maxZoom))
|
||||
Slider(value: self.$maxZoom, in: (self.useMinZoomBoundary ? self.minZoom : 0)...3000000, step: 10)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Section(header: Text("Rotation")) {
|
||||
Toggle("Rotation enabled", isOn: self.$rotationEnabled)
|
||||
Toggle("Show compass when rotated", isOn: self.$showCompassWhenRotated)
|
||||
}
|
||||
|
||||
Section {
|
||||
Toggle("Show map Center", isOn: self.$showMapCenter)
|
||||
}
|
||||
|
||||
Section(header: Text("User Location")) {
|
||||
Toggle("Show User Location", isOn: self.$showUserLocation)
|
||||
Picker("Follow Mode", selection: self.$userTrackingMode) {
|
||||
Text("Nicht folgen").tag(MKUserTrackingMode.none)
|
||||
Text("Folgen").tag(MKUserTrackingMode.follow)
|
||||
Text("Richtung folgen").tag(MKUserTrackingMode.followWithHeading)
|
||||
}.pickerStyle(MenuPickerStyle())
|
||||
|
||||
}
|
||||
|
||||
Section(header: Text("Annotations")) {
|
||||
Toggle("Show Annotations", isOn: self.$showAnnotations)
|
||||
Button("Add Annotation") {
|
||||
let annotation = MKPointAnnotation()
|
||||
annotation.coordinate = self.mapRegion.center
|
||||
annotation.title = "Title"
|
||||
annotation.subtitle = "Subtitle"
|
||||
self.annotations.append(annotation)
|
||||
}
|
||||
|
||||
Button("Delete all") { self.annotations = [] }.foregroundColor(.red)
|
||||
}
|
||||
|
||||
Section(header: Text("Overlays")) {
|
||||
Toggle("Show Overlays", isOn: self.$showOverlays)
|
||||
Button("Add circle") {
|
||||
self.overlays.append(MapView.Overlay(
|
||||
shape: MKCircle(
|
||||
center: self.mapRegion.center,
|
||||
radius: 20
|
||||
),
|
||||
strokeColor: UIColor.systemBlue,
|
||||
lineWidth: 10
|
||||
))
|
||||
}
|
||||
|
||||
Button("Delete all") { self.overlays = [] }.foregroundColor(.red)
|
||||
}
|
||||
|
||||
Section(header: Text("Custom Map Overlay")) {
|
||||
Button("Keine") { self.customMapOverlay = nil }
|
||||
Button("OSM Online") {
|
||||
self.customMapOverlay = MapView.CustomMapOverlay(
|
||||
mapName: "https://tile.openstreetmap.org/",
|
||||
tileType: "png",
|
||||
canReplaceMapContent: true
|
||||
)
|
||||
}
|
||||
Button("Farm Map") {
|
||||
self.customMapOverlay = MapView.CustomMapOverlay(
|
||||
mapName: "http://10.147.253.250:5050/local/map/",
|
||||
tileType: "png",
|
||||
canReplaceMapContent: true
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
}.listStyle(GroupedListStyle())
|
||||
.navigationBarTitle("Map Configuration", displayMode: NavigationBarItem.TitleDisplayMode.inline)
|
||||
|
||||
ZStack {
|
||||
|
||||
MapView(
|
||||
region: self.$mapRegion,
|
||||
customMapOverlay: self.customMapOverlay,
|
||||
mapType: self.mapType,
|
||||
zoomEnabled: self.zoomEnabled,
|
||||
showZoomScale: self.showZoomScale,
|
||||
zoomRange: (minHeight: self.useMinZoomBoundary ? self.minZoom : 0, maxHeight: self.useMaxZoomBoundary ? self.maxZoom : .infinity),
|
||||
scrollEnabled: self.scrollEnabled,
|
||||
scrollBoundaries: self.useScrollBoundaries ? self.scrollBoundaries : nil,
|
||||
rotationEnabled: self.rotationEnabled,
|
||||
showCompassWhenRotated: self.showCompassWhenRotated,
|
||||
showUserLocation: self.showUserLocation,
|
||||
userTrackingMode: self.userTrackingMode,
|
||||
userLocation: self.$userLocation,
|
||||
annotations: self.showAnnotations ? self.annotations : [],
|
||||
overlays: self.showOverlays ? self.overlays : []
|
||||
)
|
||||
|
||||
VStack {
|
||||
|
||||
Spacer()
|
||||
|
||||
HStack {
|
||||
if let userLocation = self.userLocation, self.showUserLocation {
|
||||
VStack(alignment: .leading) {
|
||||
Button("Center user location") {
|
||||
self.mapRegion.center = userLocation
|
||||
}
|
||||
Text("User Location").bold()
|
||||
Text("\(userLocation.latitude)")
|
||||
Text("\(userLocation.longitude)")
|
||||
}
|
||||
}
|
||||
|
||||
Spacer()
|
||||
|
||||
VStack(alignment: .leading) {
|
||||
Text("Map Center").bold()
|
||||
Text("\(self.mapRegion.center.latitude)")
|
||||
Text("\(self.mapRegion.center.longitude)")
|
||||
}
|
||||
}
|
||||
|
||||
Picker("", selection: self.$mapType) {
|
||||
Text("Standard").tag(MKMapType.standard)
|
||||
Text("Muted Standard").tag(MKMapType.mutedStandard)
|
||||
Text("Satellite").tag(MKMapType.satellite)
|
||||
Text("Satellite Flyover").tag(MKMapType.satelliteFlyover)
|
||||
Text("Hybrid").tag(MKMapType.hybrid)
|
||||
Text("Hybrid Flyover").tag(MKMapType.hybridFlyover)
|
||||
}.pickerStyle(SegmentedPickerStyle())
|
||||
|
||||
if self.showMapCenter {
|
||||
Circle().frame(width: 8, height: 8).foregroundColor(.red)
|
||||
}
|
||||
|
||||
}.padding()
|
||||
|
||||
}.navigationBarTitle("SwiftUI MapView", displayMode: NavigationBarItem.TitleDisplayMode.inline)
|
||||
.ignoresSafeArea(edges: .bottom)
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
public struct MapView_Previews: PreviewProvider {
|
||||
|
||||
public static var previews: some View {
|
||||
|
||||
MapViewDemo()
|
||||
|
||||
}
|
||||
|
||||
}*/
|
||||
#endif
|
||||
244
Meshtastic/Views/Map/WaypointFormView.swift
Normal file
244
Meshtastic/Views/Map/WaypointFormView.swift
Normal file
|
|
@ -0,0 +1,244 @@
|
|||
//
|
||||
// WaypointFormView.swift
|
||||
// Meshtastic
|
||||
//
|
||||
// Copyright Garth Vander Houwen 1/10/23.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
import CoreLocation
|
||||
|
||||
struct WaypointFormView: View {
|
||||
|
||||
@EnvironmentObject var bleManager: BLEManager
|
||||
@Environment(\.dismiss) private var dismiss
|
||||
@State var coordinate: CLLocationCoordinate2D
|
||||
@State var waypointId : Int = 0
|
||||
|
||||
@FocusState private var iconIsFocused: Bool
|
||||
|
||||
@State private var name: String = ""
|
||||
@State private var description: String = ""
|
||||
@State private var icon: String = "📍"
|
||||
@State private var latitude: Double = 0
|
||||
@State private var longitude: Double = 0
|
||||
@State private var expires: Bool = false
|
||||
@State private var expire: Date = Date() // = Date.now.addingTimeInterval(60 * 120) // 1 minute * 120 = 2 Hours
|
||||
@State private var locked: Bool = false
|
||||
@State private var lockedTo: Int64 = 0
|
||||
|
||||
var body: some View {
|
||||
|
||||
Form {
|
||||
let distance = CLLocation(latitude: LocationHelper.currentLocation.latitude, longitude: LocationHelper.currentLocation.longitude).distance(from: CLLocation(latitude: coordinate.latitude, longitude: coordinate.longitude))
|
||||
Section(header: Text((waypointId > 0) ? "Editing Waypoint" : "Create Waypoint")) {
|
||||
HStack {
|
||||
Text("Location: \(String(format: "%.5f", latitude) + "," + String(format: "%.5f", longitude))")
|
||||
.textSelection(.enabled)
|
||||
.foregroundColor(Color.gray)
|
||||
.font(.caption2)
|
||||
if coordinate.latitude != LocationHelper.DefaultLocation.latitude && coordinate.longitude != LocationHelper.DefaultLocation.longitude {
|
||||
DistanceText(meters: distance)
|
||||
.foregroundColor(Color.gray)
|
||||
.font(.caption2)
|
||||
}
|
||||
}
|
||||
HStack {
|
||||
Text("Name")
|
||||
Spacer()
|
||||
TextField(
|
||||
"Name",
|
||||
text: $name,
|
||||
axis: .vertical
|
||||
)
|
||||
.foregroundColor(Color.gray)
|
||||
.onChange(of: name, perform: { value in
|
||||
let totalBytes = name.utf8.count
|
||||
// Only mess with the value if it is too big
|
||||
if totalBytes > 30 {
|
||||
let firstNBytes = Data(name.utf8.prefix(30))
|
||||
if let maxBytesString = String(data: firstNBytes, encoding: String.Encoding.utf8) {
|
||||
// Set the name back to the last place where it was the right size
|
||||
name = maxBytesString
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
HStack {
|
||||
Text("Description")
|
||||
Spacer()
|
||||
TextField(
|
||||
"Description",
|
||||
text: $description,
|
||||
axis: .vertical
|
||||
)
|
||||
.foregroundColor(Color.gray)
|
||||
.onChange(of: description, perform: { value in
|
||||
let totalBytes = description.utf8.count
|
||||
// Only mess with the value if it is too big
|
||||
if totalBytes > 100 {
|
||||
let firstNBytes = Data(description.utf8.prefix(100))
|
||||
if let maxBytesString = String(data: firstNBytes, encoding: String.Encoding.utf8) {
|
||||
// Set the name back to the last place where it was the right size
|
||||
description = maxBytesString
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
HStack {
|
||||
Text("Icon")
|
||||
Spacer()
|
||||
EmojiOnlyTextField(text: $icon, placeholder: "Select an emoji")
|
||||
.font(.title)
|
||||
.focused($iconIsFocused)
|
||||
.onChange(of: icon) { value in
|
||||
|
||||
// If you have anything other than emojis in your string make it empty
|
||||
if !value.onlyEmojis() {
|
||||
icon = ""
|
||||
}
|
||||
// If a second emoji is entered delete the first one
|
||||
if value.count >= 1 {
|
||||
|
||||
if value.count > 1 {
|
||||
let index = value.index(value.startIndex, offsetBy: 1)
|
||||
icon = String(value[index])
|
||||
}
|
||||
iconIsFocused = false
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
// Toggle(isOn: $expires) {
|
||||
// Label("Expires", systemImage: "clock.badge.xmark")
|
||||
// }
|
||||
// .toggleStyle(SwitchToggleStyle(tint: .accentColor))
|
||||
// if expires {
|
||||
// DatePicker("Expire", selection: $expire, in: Date.now...)
|
||||
// .datePickerStyle(.compact)
|
||||
// .font(.callout)
|
||||
// }
|
||||
Toggle(isOn: $locked) {
|
||||
Label("Locked", systemImage: "lock")
|
||||
}
|
||||
.toggleStyle(SwitchToggleStyle(tint: .accentColor))
|
||||
}
|
||||
}
|
||||
HStack {
|
||||
Button {
|
||||
|
||||
var newWaypoint = Waypoint()
|
||||
|
||||
if waypointId > 0 {
|
||||
newWaypoint.id = UInt32(waypointId)
|
||||
} else {
|
||||
newWaypoint.id = UInt32.random(in: UInt32(UInt8.max)..<UInt32.max)
|
||||
}
|
||||
newWaypoint.name = name.count > 0 ? name : "Dropped Pin"
|
||||
newWaypoint.description_p = description
|
||||
newWaypoint.latitudeI = Int32(coordinate.latitude * 1e7)
|
||||
newWaypoint.longitudeI = Int32(coordinate.longitude * 1e7)
|
||||
// Unicode scalar value for the icon emoji string
|
||||
let unicodeScalers = icon.unicodeScalars
|
||||
// First element as an UInt32
|
||||
let unicode = unicodeScalers[unicodeScalers.startIndex].value
|
||||
newWaypoint.icon = unicode
|
||||
if locked {
|
||||
|
||||
if lockedTo == 0 {
|
||||
newWaypoint.lockedTo = UInt32(bleManager.connectedPeripheral!.num)
|
||||
} else {
|
||||
newWaypoint.lockedTo = UInt32(lockedTo)
|
||||
}
|
||||
}
|
||||
if expire.timeIntervalSince1970 > 0 {
|
||||
newWaypoint.expire = UInt32(expire.timeIntervalSince1970)
|
||||
}
|
||||
if bleManager.sendWaypoint(waypoint: newWaypoint) {
|
||||
waypointId = 0
|
||||
dismiss()
|
||||
} else {
|
||||
waypointId = 0
|
||||
dismiss()
|
||||
print("Send waypoint failed")
|
||||
}
|
||||
} label: {
|
||||
Label("Send", systemImage: "arrow.up")
|
||||
}
|
||||
.buttonStyle(.bordered)
|
||||
.buttonBorderShape(.capsule)
|
||||
.controlSize(.large)
|
||||
.disabled(bleManager.connectedPeripheral == nil)
|
||||
.padding(.bottom)
|
||||
|
||||
Button(role:.cancel) {
|
||||
dismiss()
|
||||
} label: {
|
||||
Label("cancel", systemImage: "x.circle")
|
||||
}
|
||||
.buttonStyle(.bordered)
|
||||
.buttonBorderShape(.capsule)
|
||||
.controlSize(.large)
|
||||
.padding(.bottom)
|
||||
|
||||
if waypointId > 0 {
|
||||
Button(role: .destructive) {
|
||||
let waypoint = getWaypoint(id: Int64(waypointId), context: bleManager.context!)
|
||||
bleManager.context!.delete(waypoint)
|
||||
do {
|
||||
try bleManager.context!.save()
|
||||
} catch {
|
||||
bleManager.context!.rollback()
|
||||
}
|
||||
dismiss()
|
||||
} label: {
|
||||
Label("delete", systemImage: "trash")
|
||||
}
|
||||
.buttonStyle(.bordered)
|
||||
.buttonBorderShape(.capsule)
|
||||
.controlSize(.large)
|
||||
.padding(.bottom)
|
||||
}
|
||||
}
|
||||
.onChange(of: waypointId) { newId in
|
||||
print(newId)
|
||||
|
||||
}
|
||||
.onAppear {
|
||||
if waypointId > 0 {
|
||||
let waypoint = getWaypoint(id: Int64(waypointId), context: bleManager.context!)
|
||||
waypointId = Int(waypoint.id)
|
||||
name = waypoint.name ?? "Dropped Pin"
|
||||
description = waypoint.longDescription ?? ""
|
||||
icon = String(UnicodeScalar(Int(waypoint.icon)) ?? "📍")
|
||||
latitude = Double(waypoint.latitudeI) / 1e7
|
||||
longitude = Double(waypoint.longitudeI) / 1e7
|
||||
if waypoint.expire != nil {
|
||||
expires = true
|
||||
expire = waypoint.expire ?? Date()
|
||||
} else {
|
||||
expires = false
|
||||
}
|
||||
if waypoint.locked > 0 {
|
||||
locked = true
|
||||
lockedTo = waypoint.locked
|
||||
}
|
||||
} else {
|
||||
name = ""
|
||||
description = ""
|
||||
locked = false
|
||||
expires = false
|
||||
expire = Date.now.addingTimeInterval(60 * 120)
|
||||
icon = "📍"
|
||||
latitude = coordinate.latitude
|
||||
longitude = coordinate.longitude
|
||||
}
|
||||
|
||||
if coordinate.distance(from: LocationHelper.DefaultLocation) == 0.0 {
|
||||
// Too close to apple park, bail out
|
||||
waypointId = 0
|
||||
dismiss()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -37,7 +37,7 @@ struct DeviceMetricsLog: View {
|
|||
}
|
||||
}
|
||||
let localeDateFormat = DateFormatter.dateFormat(fromTemplate: "yyMMddjmma", options: 0, locale: Locale.current)
|
||||
let dateFormatString = (localeDateFormat ?? "MM/dd/YY j:mma")
|
||||
let dateFormatString = (localeDateFormat ?? "MM/dd/YY j:mma").replacingOccurrences(of: ",", with: "")
|
||||
if UIDevice.current.userInterfaceIdiom == .pad || UIDevice.current.userInterfaceIdiom == .mac {
|
||||
//Add a table for mac and ipad
|
||||
Table(node.telemetries!.reversed() as! [TelemetryEntity]) {
|
||||
|
|
@ -78,11 +78,11 @@ struct DeviceMetricsLog: View {
|
|||
ScrollView {
|
||||
|
||||
let columns = [
|
||||
GridItem(),
|
||||
GridItem(),
|
||||
GridItem(),
|
||||
GridItem(),
|
||||
GridItem(.fixed(140))
|
||||
GridItem(.flexible(minimum: 30, maximum: 60), spacing: 0.1),
|
||||
GridItem(.flexible(minimum: 30, maximum: 60), spacing: 0.1),
|
||||
GridItem(.flexible(minimum: 30, maximum: 70), spacing: 0.1),
|
||||
GridItem(.flexible(minimum: 30, maximum: 65), spacing: 0.1),
|
||||
GridItem(spacing: 0)
|
||||
]
|
||||
LazyVGrid(columns: columns, alignment: .leading, spacing: 1) {
|
||||
GridRow {
|
||||
|
|
@ -119,7 +119,7 @@ struct DeviceMetricsLog: View {
|
|||
Text("\(String(format: "%.2f", dm.airUtilTx))%")
|
||||
.font(.caption)
|
||||
|
||||
Text(dm.time?.formattedDate(format: dateFormatString) ?? NSLocalizedString("unknown.age", comment: ""))
|
||||
Text(dm.time?.formattedDate(format: dateFormatString) ?? "Unknown time")
|
||||
.font(.caption2)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -22,7 +22,7 @@ struct EnvironmentMetricsLog: View {
|
|||
|
||||
NavigationStack {
|
||||
let localeDateFormat = DateFormatter.dateFormat(fromTemplate: "yyMMddjmma", options: 0, locale: Locale.current)
|
||||
let dateFormatString = (localeDateFormat ?? "MM/dd/YY j:mma")
|
||||
let dateFormatString = (localeDateFormat ?? "MM/dd/YY j:mma").replacingOccurrences(of: ",", with: "")
|
||||
if UIDevice.current.userInterfaceIdiom == .pad || UIDevice.current.userInterfaceIdiom == .mac {
|
||||
//Add a table for mac and ipad
|
||||
Table(node.telemetries!.reversed() as! [TelemetryEntity]) {
|
||||
|
|
@ -65,11 +65,11 @@ struct EnvironmentMetricsLog: View {
|
|||
} else {
|
||||
ScrollView {
|
||||
let columns = [
|
||||
GridItem(),
|
||||
GridItem(),
|
||||
GridItem(),
|
||||
GridItem(),
|
||||
GridItem(.fixed(140))
|
||||
GridItem(.flexible(minimum: 30, maximum: 60), spacing: 0.1),
|
||||
GridItem(.flexible(minimum: 30, maximum: 60), spacing: 0.1),
|
||||
GridItem(.flexible(minimum: 30, maximum: 60), spacing: 0.1),
|
||||
GridItem(.flexible(minimum: 30, maximum: 60), spacing: 0.1),
|
||||
GridItem(spacing: 0)
|
||||
]
|
||||
LazyVGrid(columns: columns, alignment: .leading, spacing: 1) {
|
||||
|
||||
|
|
|
|||
|
|
@ -1,73 +1,88 @@
|
|||
/*
|
||||
Abstract:
|
||||
A view showing the details for a node.
|
||||
*/
|
||||
Abstract:
|
||||
A view showing the details for a node.
|
||||
*/
|
||||
|
||||
import SwiftUI
|
||||
import MapKit
|
||||
import CoreLocation
|
||||
|
||||
struct NodeDetail: View {
|
||||
|
||||
|
||||
@Environment(\.managedObjectContext) var context
|
||||
@EnvironmentObject var bleManager: BLEManager
|
||||
|
||||
@State private var showingDetailsPopover = false
|
||||
|
||||
@State var satsInView = 0
|
||||
@State private var mapType: MKMapType = .standard
|
||||
@State var waypointCoordinate: CLLocationCoordinate2D?
|
||||
@State var editingWaypoint: Int = 0
|
||||
@State private var showingDetailsPopover = false
|
||||
@State private var showingShutdownConfirm: Bool = false
|
||||
@State private var showingRebootConfirm: Bool = false
|
||||
|
||||
@State private var presentingWaypointForm = false
|
||||
@State private var showOverlays: Bool = true
|
||||
@State private var overlays: [MapViewSwiftUI.Overlay] = []
|
||||
@State private var customMapOverlay: MapViewSwiftUI.CustomMapOverlay? = MapViewSwiftUI.CustomMapOverlay(
|
||||
mapName: "offlinemap",
|
||||
tileType: "png",
|
||||
canReplaceMapContent: true
|
||||
)
|
||||
|
||||
var node: NodeInfoEntity
|
||||
|
||||
|
||||
@FetchRequest(sortDescriptors: [NSSortDescriptor(key: "name", ascending: false)],
|
||||
predicate: NSPredicate(
|
||||
format: "expire == nil || expire >= %@", Date() as NSDate
|
||||
), animation: .easeIn)
|
||||
private var waypoints: FetchedResults<WaypointEntity>
|
||||
|
||||
var body: some View {
|
||||
|
||||
let hwModelString = node.user?.hwModel ?? "UNSET"
|
||||
|
||||
|
||||
NavigationStack {
|
||||
GeometryReader { bounds in
|
||||
VStack {
|
||||
if node.positions?.count ?? 0 > 0 {
|
||||
let mostRecent = node.positions?.lastObject as! PositionEntity
|
||||
if mostRecent.coordinate != nil {
|
||||
let nodeCoordinatePosition = CLLocationCoordinate2D(latitude: mostRecent.latitude!, longitude: mostRecent.longitude!)
|
||||
|
||||
let regionBinding = Binding<MKCoordinateRegion>(
|
||||
get: {
|
||||
MKCoordinateRegion(center: nodeCoordinatePosition, span: MKCoordinateSpan(latitudeDelta: 0.005, longitudeDelta: 0.005))
|
||||
},
|
||||
set: { _ in }
|
||||
)
|
||||
let nodeCoordinatePosition = CLLocationCoordinate2D(latitude: mostRecent.latitude!, longitude: mostRecent.longitude!)
|
||||
ZStack {
|
||||
let annotations = node.positions?.array as! [PositionEntity]
|
||||
ZStack {
|
||||
let annotations = node.positions?.array as! [PositionEntity]
|
||||
|
||||
Map(coordinateRegion: regionBinding,
|
||||
interactionModes: [.all],
|
||||
showsUserLocation: true,
|
||||
userTrackingMode: .constant(.follow),
|
||||
annotationItems: annotations) { location in
|
||||
MapViewSwiftUI(onLongPress: { coord in
|
||||
waypointCoordinate = coord
|
||||
editingWaypoint = 0
|
||||
presentingWaypointForm = true
|
||||
}, onWaypointEdit: { wpId in
|
||||
if wpId > 0 {
|
||||
editingWaypoint = wpId
|
||||
presentingWaypointForm = true
|
||||
}
|
||||
}, positions: annotations, waypoints: Array(waypoints), mapViewType: mapType,
|
||||
centerOnPositionsOnly: true,
|
||||
customMapOverlay: self.customMapOverlay,
|
||||
overlays: self.overlays
|
||||
|
||||
return MapAnnotation(
|
||||
coordinate: location.coordinate ?? CLLocationCoordinate2D(latitude: 0, longitude: 0),
|
||||
content: {
|
||||
|
||||
NodeAnnotation(time: location.time!)
|
||||
)
|
||||
VStack {
|
||||
Spacer()
|
||||
Text(mostRecent.satsInView > 0 ? "Sats: \(mostRecent.satsInView)" : " ")
|
||||
.font(.caption)
|
||||
.offset(y: 20)
|
||||
Picker("Map Type", selection: $mapType) {
|
||||
ForEach(MeshMapType.allCases) { map in
|
||||
Text(map.description).tag(map.MKMapTypeValue())
|
||||
}
|
||||
)
|
||||
}
|
||||
.pickerStyle(.menu)
|
||||
}
|
||||
.ignoresSafeArea(.all, edges: [.leading, .trailing])
|
||||
.frame(idealWidth: bounds.size.width, minHeight: bounds.size.height / 2)
|
||||
|
||||
}
|
||||
Text(mostRecent.satsInView > 0 ? "Sats: \(mostRecent.satsInView)" : " ")
|
||||
.offset( y:-40)
|
||||
.ignoresSafeArea(.all, edges: [.top, .leading, .trailing])
|
||||
.frame(idealWidth: bounds.size.width, minHeight: bounds.size.height / 1.65)
|
||||
}
|
||||
|
||||
} else {
|
||||
HStack {
|
||||
|
||||
}
|
||||
.padding([.top], 60)
|
||||
.padding([.top], 20)
|
||||
}
|
||||
|
||||
ScrollView {
|
||||
|
|
@ -80,13 +95,12 @@ struct NodeDetail: View {
|
|||
Divider()
|
||||
VStack {
|
||||
if node.user != nil {
|
||||
|
||||
Image(hwModelString)
|
||||
.resizable()
|
||||
.aspectRatio(contentMode: .fill)
|
||||
.frame(width: 100, height: 100)
|
||||
.cornerRadius(5)
|
||||
|
||||
|
||||
Text(String(hwModelString))
|
||||
.foregroundColor(.gray)
|
||||
.font(.largeTitle).fixedSize()
|
||||
|
|
@ -96,7 +110,7 @@ struct NodeDetail: View {
|
|||
if node.snr > 0 {
|
||||
Divider()
|
||||
VStack(alignment: .center) {
|
||||
|
||||
|
||||
Image(systemName: "waveform.path")
|
||||
.font(.title)
|
||||
.foregroundColor(.accentColor)
|
||||
|
|
@ -109,15 +123,15 @@ struct NodeDetail: View {
|
|||
.fixedSize()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
if node.telemetries?.count ?? 0 >= 1 {
|
||||
|
||||
|
||||
let mostRecent = node.telemetries?.lastObject as! TelemetryEntity
|
||||
Divider()
|
||||
VStack(alignment: .center) {
|
||||
BatteryGauge(batteryLevel: Double(mostRecent.batteryLevel))
|
||||
if mostRecent.voltage > 0 {
|
||||
|
||||
|
||||
Text(String(format: "%.2f", mostRecent.voltage) + " V")
|
||||
.font(.title)
|
||||
.foregroundColor(.gray)
|
||||
|
|
@ -140,14 +154,13 @@ struct NodeDetail: View {
|
|||
.symbolRenderingMode(.hierarchical)
|
||||
Text("user").font(.title)+Text(":").font(.title)
|
||||
}
|
||||
//Text(node.user?.userId ?? "??????").font(.title).foregroundColor(.gray)
|
||||
Text("!\(String(format:"%02x", node.num))")
|
||||
.font(.title).foregroundColor(.gray)
|
||||
}
|
||||
Divider()
|
||||
VStack {
|
||||
HStack {
|
||||
Image(systemName: "number")
|
||||
Image(systemName: "number")
|
||||
.font(.title2)
|
||||
.foregroundColor(.accentColor)
|
||||
.symbolRenderingMode(.hierarchical)
|
||||
|
|
@ -160,8 +173,8 @@ struct NodeDetail: View {
|
|||
HStack {
|
||||
Image(systemName: "globe")
|
||||
.font(.title)
|
||||
.foregroundColor(.accentColor)
|
||||
.symbolRenderingMode(.hierarchical)
|
||||
.foregroundColor(.accentColor)
|
||||
.symbolRenderingMode(.hierarchical)
|
||||
Text("MAC Address: ").font(.title)
|
||||
|
||||
}
|
||||
|
|
@ -174,8 +187,8 @@ struct NodeDetail: View {
|
|||
HStack {
|
||||
Image(systemName: "clock.badge.checkmark.fill")
|
||||
.font(.title)
|
||||
.foregroundColor(.accentColor)
|
||||
.symbolRenderingMode(.hierarchical)
|
||||
.foregroundColor(.accentColor)
|
||||
.symbolRenderingMode(.hierarchical)
|
||||
Text("heard.last").font(.title)+Text(":").font(.title)
|
||||
|
||||
}
|
||||
|
|
@ -184,13 +197,12 @@ struct NodeDetail: View {
|
|||
.foregroundColor(.gray)
|
||||
}
|
||||
}
|
||||
.padding()
|
||||
Divider()
|
||||
|
||||
} else {
|
||||
|
||||
HStack {
|
||||
|
||||
|
||||
VStack(alignment: .center) {
|
||||
CircleText(text: node.user?.shortName ?? "???", color: .accentColor)
|
||||
}
|
||||
|
|
@ -205,12 +217,11 @@ struct NodeDetail: View {
|
|||
.font(.callout).fixedSize()
|
||||
}
|
||||
}
|
||||
.padding(5)
|
||||
|
||||
if node.snr > 0 {
|
||||
Divider()
|
||||
VStack(alignment: .center) {
|
||||
|
||||
|
||||
Image(systemName: "waveform.path")
|
||||
.font(.title)
|
||||
.foregroundColor(.accentColor)
|
||||
|
|
@ -221,19 +232,13 @@ struct NodeDetail: View {
|
|||
.foregroundColor(.gray)
|
||||
.fixedSize()
|
||||
}
|
||||
.padding(5)
|
||||
}
|
||||
|
||||
|
||||
if node.telemetries?.count ?? 0 >= 1 {
|
||||
|
||||
let mostRecent = node.telemetries?.lastObject as! TelemetryEntity
|
||||
|
||||
Divider()
|
||||
|
||||
VStack(alignment: .center) {
|
||||
|
||||
BatteryGauge(batteryLevel: Double(mostRecent.batteryLevel))
|
||||
|
||||
if mostRecent.voltage > 0 {
|
||||
|
||||
Text(String(format: "%.2f", mostRecent.voltage) + " V")
|
||||
|
|
@ -241,14 +246,11 @@ struct NodeDetail: View {
|
|||
.foregroundColor(.gray)
|
||||
.fixedSize()
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
Divider()
|
||||
HStack(alignment: .center) {
|
||||
|
||||
|
||||
VStack {
|
||||
HStack {
|
||||
Image(systemName: "person")
|
||||
|
|
@ -262,7 +264,7 @@ struct NodeDetail: View {
|
|||
Divider()
|
||||
VStack {
|
||||
HStack {
|
||||
Image(systemName: "number")
|
||||
Image(systemName: "number")
|
||||
.font(.title2)
|
||||
.foregroundColor(.accentColor)
|
||||
.symbolRenderingMode(.hierarchical)
|
||||
|
|
@ -271,17 +273,16 @@ struct NodeDetail: View {
|
|||
Text(String(node.num)).font(.title3).foregroundColor(.gray)
|
||||
}
|
||||
}
|
||||
.padding(5)
|
||||
Divider()
|
||||
HStack {
|
||||
Image(systemName: "globe")
|
||||
.font(.headline)
|
||||
.foregroundColor(.accentColor)
|
||||
.symbolRenderingMode(.hierarchical)
|
||||
.font(.headline)
|
||||
.foregroundColor(.accentColor)
|
||||
.symbolRenderingMode(.hierarchical)
|
||||
Text("MAC Address: ")
|
||||
Text(String(node.user?.macaddr?.macAddressString ?? "not a valid mac address")).foregroundColor(.gray)
|
||||
}
|
||||
.padding([.bottom], 0)
|
||||
.padding([.bottom], 10)
|
||||
Divider()
|
||||
}
|
||||
|
||||
|
|
@ -334,16 +335,16 @@ struct NodeDetail: View {
|
|||
}
|
||||
|
||||
if self.bleManager.connectedPeripheral != nil && self.bleManager.connectedPeripheral.num == node.num && self.bleManager.connectedPeripheral.num == node.num {
|
||||
|
||||
|
||||
HStack {
|
||||
|
||||
if hwModelString == "TBEAM" || hwModelString == "TECHO" || hwModelString.contains("4631") {
|
||||
|
||||
|
||||
Button(action: {
|
||||
|
||||
showingShutdownConfirm = true
|
||||
}) {
|
||||
|
||||
|
||||
Label("Power Off", systemImage: "power")
|
||||
}
|
||||
.buttonStyle(.bordered)
|
||||
|
|
@ -361,13 +362,10 @@ struct NodeDetail: View {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Button(action: {
|
||||
|
||||
showingRebootConfirm = true
|
||||
|
||||
}) {
|
||||
|
||||
Label("reboot", systemImage: "arrow.triangle.2.circlepath")
|
||||
}
|
||||
.buttonStyle(.bordered)
|
||||
|
|
@ -375,33 +373,33 @@ struct NodeDetail: View {
|
|||
.controlSize(.large)
|
||||
.padding()
|
||||
.confirmationDialog("are.you.sure",
|
||||
|
||||
isPresented: $showingRebootConfirm
|
||||
) {
|
||||
Button("reboot.node", role: .destructive) {
|
||||
|
||||
if !bleManager.sendReboot(fromUser: node.user!, toUser: node.user!) {
|
||||
print("Reboot Failed")
|
||||
}
|
||||
}
|
||||
isPresented: $showingRebootConfirm
|
||||
) {
|
||||
Button("reboot.node", role: .destructive) {
|
||||
if !bleManager.sendReboot(fromUser: node.user!, toUser: node.user!) {
|
||||
print("Reboot Failed")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.padding(5)
|
||||
}
|
||||
}
|
||||
.offset( y:-40)
|
||||
}
|
||||
.edgesIgnoringSafeArea([.leading, .trailing])
|
||||
.sheet(isPresented: $presentingWaypointForm ) {//, onDismiss: didDismissSheet) {
|
||||
WaypointFormView(coordinate: waypointCoordinate ?? LocationHelper.DefaultLocation, waypointId: editingWaypoint)
|
||||
.presentationDetents([.medium, .large])
|
||||
.presentationDragIndicator(.automatic)
|
||||
}
|
||||
.navigationBarTitle(String(node.user?.longName ?? NSLocalizedString("unknown", comment: "")), displayMode: .inline)
|
||||
.padding(.bottom, 10)
|
||||
.navigationBarItems(trailing:
|
||||
ZStack {
|
||||
ConnectedDevice(
|
||||
bluetoothOn: bleManager.isSwitchedOn,
|
||||
deviceConnected: bleManager.connectedPeripheral != nil,
|
||||
name: (bleManager.connectedPeripheral != nil) ? bleManager.connectedPeripheral.shortName : "????")
|
||||
}
|
||||
)
|
||||
ConnectedDevice(
|
||||
bluetoothOn: bleManager.isSwitchedOn,
|
||||
deviceConnected: bleManager.connectedPeripheral != nil,
|
||||
name: (bleManager.connectedPeripheral != nil) ? bleManager.connectedPeripheral.shortName : "????")
|
||||
})
|
||||
.onAppear {
|
||||
self.bleManager.context = context
|
||||
}
|
||||
|
|
|
|||
|
|
@ -53,8 +53,8 @@ struct NodeList: View {
|
|||
HStack(alignment: .bottom) {
|
||||
let lastPostion = node.positions!.reversed()[0] as! PositionEntity
|
||||
let myCoord = CLLocation(latitude: LocationHelper.currentLocation.latitude, longitude: LocationHelper.currentLocation.longitude)
|
||||
if lastPostion.coordinate != nil && myCoord.coordinate.longitude != LocationHelper.DefaultLocation.longitude && myCoord.coordinate.latitude != LocationHelper.DefaultLocation.latitude {
|
||||
let nodeCoord = CLLocation(latitude: lastPostion.coordinate!.latitude, longitude: lastPostion.coordinate!.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(.title3)
|
||||
|
|
|
|||
|
|
@ -15,14 +15,13 @@ struct NodeMap: View {
|
|||
@Environment(\.managedObjectContext) var context
|
||||
@EnvironmentObject var bleManager: BLEManager
|
||||
@EnvironmentObject var userSettings: UserSettings
|
||||
|
||||
@AppStorage("meshMapType") var type: String = "hybrid"
|
||||
@AppStorage("meshMapCustomTileServer") var customTileServer: String = "" {
|
||||
didSet {
|
||||
if customTileServer == "" {
|
||||
self.customMapOverlay = nil
|
||||
} else {
|
||||
self.customMapOverlay = MapView.CustomMapOverlay(
|
||||
self.customMapOverlay = MapViewSwiftUI.CustomMapOverlay(
|
||||
mapName: customTileServer,
|
||||
tileType: "png",
|
||||
canReplaceMapContent: true
|
||||
|
|
@ -30,94 +29,72 @@ struct NodeMap: View {
|
|||
}
|
||||
}
|
||||
}
|
||||
@FetchRequest(sortDescriptors: [NSSortDescriptor(key: "time", ascending: false)],
|
||||
predicate: NSPredicate(format: "time >= %@", Calendar.current.startOfDay(for: Date()) as NSDate), animation: .easeIn)
|
||||
private var positions: FetchedResults<PositionEntity>
|
||||
|
||||
@State private var showLabels: Bool = false
|
||||
|
||||
//@State private var annotationItems: [MapLocation] = []
|
||||
//@FetchRequest( sortDescriptors: [NSSortDescriptor(keyPath: \NodeInfoEntity.lastHeard, ascending: false)], animation: .default)
|
||||
//private var locationNodes: FetchedResults<NodeInfoEntity>
|
||||
|
||||
/*@State private var mapRegion: MKCoordinateRegion = MKCoordinateRegion(
|
||||
center: CLLocationCoordinate2D(
|
||||
latitude: -38.758247,
|
||||
longitude: 175.360208
|
||||
),
|
||||
span: MKCoordinateSpan(
|
||||
latitudeDelta: 0.01,
|
||||
longitudeDelta: 0.01
|
||||
)
|
||||
)*/
|
||||
@FetchRequest(sortDescriptors: [NSSortDescriptor(key: "name", ascending: false)],
|
||||
predicate: NSPredicate(
|
||||
format: "expire == nil || expire >= %@", Date() as NSDate
|
||||
), animation: .easeIn)
|
||||
private var waypoints: FetchedResults<WaypointEntity>
|
||||
|
||||
@State private var customMapOverlay: MapView.CustomMapOverlay? = MapView.CustomMapOverlay(
|
||||
@State private var mapType: MKMapType = .standard
|
||||
@State var waypointCoordinate: CLLocationCoordinate2D = LocationHelper.DefaultLocation
|
||||
@State var editingWaypoint: Int = 0
|
||||
@State private var presentingWaypointForm = false
|
||||
@State private var customMapOverlay: MapViewSwiftUI.CustomMapOverlay? = MapViewSwiftUI.CustomMapOverlay(
|
||||
mapName: "offlinemap",
|
||||
tileType: "png",
|
||||
canReplaceMapContent: true
|
||||
)
|
||||
|
||||
//@State private var mapType: MKMapType = MKMapType.standard
|
||||
|
||||
@State private var zoomEnabled: Bool = true
|
||||
@State private var showZoomScale: Bool = true
|
||||
@State private var useMinZoomBoundary: Bool = false
|
||||
@State private var minZoom: Double = 0
|
||||
@State private var useMaxZoomBoundary: Bool = false
|
||||
@State private var maxZoom: Double = 3000000
|
||||
|
||||
@State private var scrollEnabled: Bool = true
|
||||
@State private var useScrollBoundaries: Bool = false
|
||||
@State private var scrollBoundaries: MKCoordinateRegion = MKCoordinateRegion()
|
||||
|
||||
@State private var rotationEnabled: Bool = true
|
||||
@State private var showCompassWhenRotated: Bool = true
|
||||
|
||||
@State private var showUserLocation: Bool = true
|
||||
@State private var userTrackingMode: MKUserTrackingMode = MKUserTrackingMode.none
|
||||
@State private var userLocation: CLLocationCoordinate2D? = LocationHelper.currentLocation
|
||||
|
||||
@State private var showAnnotations: Bool = true
|
||||
@State private var annotations: [MKPointAnnotation] = []
|
||||
|
||||
@State private var showOverlays: Bool = true
|
||||
@State private var overlays: [MapView.Overlay] = []
|
||||
|
||||
@State private var showMapCenter: Bool = false
|
||||
@State private var overlays: [MapViewSwiftUI.Overlay] = []
|
||||
|
||||
var body: some View {
|
||||
|
||||
NavigationStack {
|
||||
ZStack {
|
||||
|
||||
//MapView(nodes: self.locationNodes)//.environmentObject(bleManager)
|
||||
// }
|
||||
MapView(
|
||||
//region: self.$mapRegion,
|
||||
ZStack {
|
||||
|
||||
MapViewSwiftUI(onLongPress: { coord in
|
||||
waypointCoordinate = coord
|
||||
editingWaypoint = 0
|
||||
if waypointCoordinate.distance(from: LocationHelper.DefaultLocation) == 0.0 {
|
||||
print("Apple Park")
|
||||
} else {
|
||||
presentingWaypointForm = true
|
||||
}
|
||||
}, onWaypointEdit: { wpId in
|
||||
if wpId > 0 {
|
||||
editingWaypoint = wpId
|
||||
presentingWaypointForm = true
|
||||
}
|
||||
}, positions: Array(positions), waypoints: Array(waypoints), mapViewType: mapType,
|
||||
centerOnPositionsOnly: false,
|
||||
customMapOverlay: self.customMapOverlay,
|
||||
mapType: self.type,
|
||||
zoomEnabled: self.zoomEnabled,
|
||||
showZoomScale: self.showZoomScale,
|
||||
zoomRange: (minHeight: self.useMinZoomBoundary ? self.minZoom : 0, maxHeight: self.useMaxZoomBoundary ? self.maxZoom : .infinity),
|
||||
scrollEnabled: self.scrollEnabled,
|
||||
scrollBoundaries: self.useScrollBoundaries ? self.scrollBoundaries : nil,
|
||||
rotationEnabled: self.rotationEnabled,
|
||||
showCompassWhenRotated: self.showCompassWhenRotated,
|
||||
showUserLocation: self.showUserLocation,
|
||||
userTrackingMode: self.userTrackingMode,
|
||||
userLocation: self.$userLocation,
|
||||
//annotations: self.annotations,
|
||||
//locationNodes: self.locationNodes.map({ nodeinfo in return nodeinfo }),
|
||||
overlays: self.overlays
|
||||
//context: self.context
|
||||
)
|
||||
|
||||
.frame(maxHeight: .infinity)
|
||||
.ignoresSafeArea(.all, edges: [.top, .leading, .trailing])
|
||||
VStack {
|
||||
Spacer()
|
||||
Picker("Map Type", selection: $mapType) {
|
||||
ForEach(MeshMapType.allCases) { map in
|
||||
Text(map.description).tag(map.MKMapTypeValue())
|
||||
}
|
||||
}
|
||||
.pickerStyle(.menu)
|
||||
}
|
||||
}
|
||||
.ignoresSafeArea(.all, edges: [.top, .leading, .trailing])
|
||||
.frame(maxHeight: .infinity)
|
||||
.sheet(isPresented: $presentingWaypointForm ) {//, onDismiss: didDismissSheet) {
|
||||
WaypointFormView(coordinate: waypointCoordinate, waypointId: editingWaypoint)
|
||||
.presentationDetents([.medium, .large])
|
||||
.presentationDragIndicator(.automatic)
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
.navigationBarItems(leading:
|
||||
MeshtasticLogo(), trailing:
|
||||
ZStack {
|
||||
|
||||
ConnectedDevice(
|
||||
bluetoothOn: bleManager.isSwitchedOn,
|
||||
deviceConnected: bleManager.connectedPeripheral != nil,
|
||||
|
|
@ -125,18 +102,8 @@ struct NodeMap: View {
|
|||
"????")
|
||||
})
|
||||
.onAppear(perform: {
|
||||
|
||||
self.bleManager.context = context
|
||||
self.bleManager.userSettings = userSettings
|
||||
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
struct NodeMap_Previews: PreviewProvider {
|
||||
static let bleManager = BLEManager()
|
||||
|
||||
static var previews: some View {
|
||||
NodeMap()
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -22,7 +22,7 @@ struct PositionLog: View {
|
|||
|
||||
NavigationStack {
|
||||
let localeDateFormat = DateFormatter.dateFormat(fromTemplate: "yyMMddjmma", options: 0, locale: Locale.current)
|
||||
let dateFormatString = (localeDateFormat ?? "MM/dd/YY j:mma")
|
||||
let dateFormatString = (localeDateFormat ?? "MM/dd/YY j:mma").replacingOccurrences(of: ",", with: "")
|
||||
|
||||
if UIDevice.current.userInterfaceIdiom == .pad || UIDevice.current.userInterfaceIdiom == .mac {
|
||||
//Add a table for mac and ipad
|
||||
|
|
@ -31,10 +31,10 @@ struct PositionLog: View {
|
|||
Text(String(position.seqNo))
|
||||
}
|
||||
TableColumn("Latitude") { position in
|
||||
Text(String(format: "%.6f", position.latitude ?? 0))
|
||||
Text(String(format: "%.5f", position.latitude ?? 0))
|
||||
}
|
||||
TableColumn("Longitude") { position in
|
||||
Text(String(format: "%.6f", position.longitude ?? 0))
|
||||
Text(String(format: "%.5f", position.longitude ?? 0))
|
||||
}
|
||||
TableColumn("Altitude") { position in
|
||||
Text(String(position.altitude))
|
||||
|
|
@ -61,11 +61,11 @@ struct PositionLog: View {
|
|||
ScrollView {
|
||||
// Use a grid on iOS as a table only shows a single column
|
||||
let columns = [
|
||||
GridItem(.fixed(90)),
|
||||
GridItem(.fixed(95)),
|
||||
GridItem(.fixed(45)),
|
||||
GridItem(.fixed(40)),
|
||||
GridItem(.fixed(140))
|
||||
GridItem(spacing: 0.1),
|
||||
GridItem(spacing: 0.1),
|
||||
GridItem(.flexible(minimum: 35, maximum: 40), spacing: 0.1),
|
||||
GridItem(.flexible(minimum: 30, maximum: 35), spacing: 0.1),
|
||||
GridItem(spacing: 0)
|
||||
]
|
||||
LazyVGrid(columns: columns, alignment: .leading, spacing: 1) {
|
||||
|
||||
|
|
@ -89,9 +89,9 @@ struct PositionLog: View {
|
|||
}
|
||||
ForEach(node.positions!.reversed() as! [PositionEntity], id: \.self) { (mappin: PositionEntity) in
|
||||
GridRow {
|
||||
Text(String(format: "%.6f", mappin.latitude ?? 0))
|
||||
Text(String(format: "%.5f", mappin.latitude ?? 0))
|
||||
.font(.caption2)
|
||||
Text(String(format: "%.6f", mappin.longitude ?? 0))
|
||||
Text(String(format: "%.5f", mappin.longitude ?? 0))
|
||||
.font(.caption2)
|
||||
Text(String(mappin.satsInView))
|
||||
.font(.caption2)
|
||||
|
|
@ -102,19 +102,15 @@ struct PositionLog: View {
|
|||
}
|
||||
}
|
||||
}
|
||||
.padding(.leading, 15)
|
||||
.padding(.trailing, 5)
|
||||
}
|
||||
.padding(.leading)
|
||||
}
|
||||
|
||||
HStack {
|
||||
|
||||
Button(role: .destructive) {
|
||||
|
||||
isPresentingClearLogConfirm = true
|
||||
|
||||
} label: {
|
||||
|
||||
Label("Clear Log", systemImage: "trash.fill")
|
||||
}
|
||||
.buttonStyle(.bordered)
|
||||
|
|
|
|||
|
|
@ -102,6 +102,9 @@ struct AppSettings: View {
|
|||
.onAppear {
|
||||
self.bleManager.context = context
|
||||
}
|
||||
.onChange(of: userSettings.provideLocation) { newProvideLocation in
|
||||
self.bleManager.sendWantConfig()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -250,16 +250,10 @@ struct Channels: View {
|
|||
let adminMessageId = bleManager.saveChannel(channel: channel, fromUser: node!.user!, toUser: node!.user!)
|
||||
|
||||
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.
|
||||
|
||||
self.isPresentingEditView = false
|
||||
channelName = ""
|
||||
hasChanges = false
|
||||
// Would rather send a getChannel but I can't seem serialize it properly yet
|
||||
bleManager.getChannel(channel: channel, fromUser: node!.user!, toUser: node!.user!)
|
||||
//bleManager.sendWantConfig()
|
||||
}
|
||||
} label: {
|
||||
Label("save", systemImage: "square.and.arrow.down")
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@ struct BluetoothConfig: View {
|
|||
@Environment(\.dismiss) private var goBack
|
||||
|
||||
var node: NodeInfoEntity?
|
||||
var connectedNode: NodeInfoEntity?
|
||||
|
||||
@State private var isPresentingSaveConfirm: Bool = false
|
||||
@State var hasChanges = false
|
||||
|
|
@ -125,6 +126,13 @@ struct BluetoothConfig: View {
|
|||
self.mode = Int(node?.bluetoothConfig?.mode ?? 0)
|
||||
self.fixedPin = String(node?.bluetoothConfig?.fixedPin ?? 123456)
|
||||
self.hasChanges = false
|
||||
|
||||
// Need to request a LoRaConfig from the remote node before allowing changes
|
||||
if node?.bluetoothConfig == nil {
|
||||
print("empty bluetooth config")
|
||||
|
||||
}
|
||||
let adminMessageId = bleManager.requestBluetoothConfig(fromUser: connectedNode!.user!, toUser: node!.user!, adminIndex: connectedNode?.myInfo?.adminIndex ?? 0)
|
||||
}
|
||||
.onChange(of: enabled) { newEnabled in
|
||||
if node != nil && node!.bluetoothConfig != nil {
|
||||
|
|
|
|||
|
|
@ -24,29 +24,20 @@ struct DisplayConfig: View {
|
|||
@State var compassNorthTop = false
|
||||
@State var flipScreen = false
|
||||
@State var oledType = 0
|
||||
@State var displayMode = 0
|
||||
|
||||
var body: some View {
|
||||
|
||||
Form {
|
||||
Section(header: Text("Device Screen")) {
|
||||
|
||||
Picker("Screen on for", selection: $screenOnSeconds ) {
|
||||
ForEach(ScreenOnIntervals.allCases) { soi in
|
||||
Text(soi.description)
|
||||
Picker("Display Mode", selection: $displayMode ) {
|
||||
ForEach(DisplayModes.allCases) { dm in
|
||||
Text(dm.description)
|
||||
}
|
||||
}
|
||||
.pickerStyle(DefaultPickerStyle())
|
||||
|
||||
Text("How long the screen remains on after the user button is pressed or messages are received.")
|
||||
.font(.caption)
|
||||
|
||||
Picker("Carousel Interval", selection: $screenCarouselInterval ) {
|
||||
ForEach(ScreenCarouselIntervals.allCases) { sci in
|
||||
Text(sci.description)
|
||||
}
|
||||
}
|
||||
.pickerStyle(DefaultPickerStyle())
|
||||
Text("Automatically toggles to the next page on the screen like a carousel, based the specified interval.")
|
||||
Text("Override automatic OLED screen detection.")
|
||||
.font(.caption)
|
||||
|
||||
Toggle(isOn: $compassNorthTop) {
|
||||
|
|
@ -64,6 +55,7 @@ struct DisplayConfig: View {
|
|||
.toggleStyle(SwitchToggleStyle(tint: .accentColor))
|
||||
Text("Flip screen vertically")
|
||||
.font(.caption)
|
||||
|
||||
Picker("OLED Type", selection: $oledType ) {
|
||||
ForEach(OledTypes.allCases) { ot in
|
||||
Text(ot.description)
|
||||
|
|
@ -74,7 +66,25 @@ struct DisplayConfig: View {
|
|||
.font(.caption)
|
||||
|
||||
}
|
||||
Section(header: Text("Format")) {
|
||||
Section(header: Text("Timing & Format")) {
|
||||
Picker("Screen on for", selection: $screenOnSeconds ) {
|
||||
ForEach(ScreenOnIntervals.allCases) { soi in
|
||||
Text(soi.description)
|
||||
}
|
||||
}
|
||||
.pickerStyle(DefaultPickerStyle())
|
||||
Text("How long the screen remains on after the user button is pressed or messages are received.")
|
||||
.font(.caption)
|
||||
|
||||
Picker("Carousel Interval", selection: $screenCarouselInterval ) {
|
||||
ForEach(ScreenCarouselIntervals.allCases) { sci in
|
||||
Text(sci.description)
|
||||
}
|
||||
}
|
||||
.pickerStyle(DefaultPickerStyle())
|
||||
Text("Automatically toggles to the next page on the screen like a carousel, based the specified interval.")
|
||||
.font(.caption)
|
||||
|
||||
Picker("GPS Format", selection: $gpsFormat ) {
|
||||
ForEach(GpsFormats.allCases) { lu in
|
||||
Text(lu.description)
|
||||
|
|
@ -116,6 +126,7 @@ struct DisplayConfig: View {
|
|||
dc.compassNorthTop = compassNorthTop
|
||||
dc.flipScreen = flipScreen
|
||||
dc.oled = OledTypes(rawValue: oledType)!.protoEnumValue()
|
||||
dc.displaymode = DisplayModes(rawValue: displayMode)!.protoEnumValue()
|
||||
|
||||
let adminMessageId = bleManager.saveDisplayConfig(config: dc, fromUser: node!.user!, toUser: node!.user!)
|
||||
if adminMessageId > 0 {
|
||||
|
|
@ -143,6 +154,7 @@ struct DisplayConfig: View {
|
|||
self.compassNorthTop = node?.displayConfig?.compassNorthTop ?? false
|
||||
self.flipScreen = node?.displayConfig?.flipScreen ?? false
|
||||
self.oledType = Int(node?.displayConfig?.oledType ?? 0)
|
||||
self.displayMode = Int(node?.displayConfig?.displayMode ?? 0)
|
||||
self.hasChanges = false
|
||||
}
|
||||
.onChange(of: screenOnSeconds) { newScreenSecs in
|
||||
|
|
@ -175,5 +187,10 @@ struct DisplayConfig: View {
|
|||
if newOledType != node!.displayConfig!.oledType { hasChanges = true }
|
||||
}
|
||||
}
|
||||
.onChange(of: displayMode) { newDisplayMode in
|
||||
if node != nil && node!.displayConfig != nil {
|
||||
if newDisplayMode != node!.displayConfig!.displayMode { hasChanges = true }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@ struct LoRaConfig: View {
|
|||
@Environment(\.dismiss) private var goBack
|
||||
|
||||
var node: NodeInfoEntity?
|
||||
var connectedNode: NodeInfoEntity?
|
||||
|
||||
@State var isPresentingSaveConfirm = false
|
||||
@State var hasChanges = false
|
||||
|
|
@ -50,6 +51,8 @@ struct LoRaConfig: View {
|
|||
}
|
||||
Section(header: Text("Mesh Options")) {
|
||||
Picker("Number of hops", selection: $hopLimit) {
|
||||
Text("Please Select")
|
||||
.tag(0)
|
||||
ForEach(HopValues.allCases) { hop in
|
||||
Text(hop.description)
|
||||
}
|
||||
|
|
@ -75,7 +78,7 @@ struct LoRaConfig: View {
|
|||
isPresented: $isPresentingSaveConfirm,
|
||||
titleVisibility: .visible
|
||||
) {
|
||||
let nodeName = bleManager.connectedPeripheral != nil ? bleManager.connectedPeripheral.longName : NSLocalizedString("unknown", comment: "Unknown")
|
||||
let nodeName = node?.user?.longName ?? NSLocalizedString("unknown", comment: "Unknown")
|
||||
let buttonText = String.localizedStringWithFormat(NSLocalizedString("save.config %@", comment: "Save Config for %@"), nodeName)
|
||||
Button(buttonText) {
|
||||
var lc = Config.LoRaConfig()
|
||||
|
|
@ -84,7 +87,7 @@ struct LoRaConfig: View {
|
|||
lc.modemPreset = ModemPresets(rawValue: modemPreset)!.protoEnumValue()
|
||||
lc.usePreset = true
|
||||
lc.txEnabled = true
|
||||
let adminMessageId = bleManager.saveLoRaConfig(config: lc, fromUser: node!.user!, toUser: node!.user!)
|
||||
let adminMessageId = bleManager.saveLoRaConfig(config: lc, fromUser: connectedNode!.user!, toUser: node!.user!, adminIndex: node?.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
|
||||
|
|
@ -102,14 +105,21 @@ struct LoRaConfig: View {
|
|||
ConnectedDevice(bluetoothOn: bleManager.isSwitchedOn, deviceConnected: bleManager.connectedPeripheral != nil, name: (bleManager.connectedPeripheral != nil) ? bleManager.connectedPeripheral.shortName : "????")
|
||||
})
|
||||
.onAppear {
|
||||
|
||||
self.bleManager.context = context
|
||||
self.hopLimit = Int(node?.loRaConfig?.hopLimit ?? 0)
|
||||
self.hopLimit = Int(node?.loRaConfig?.hopLimit ?? 3)
|
||||
self.region = Int(node?.loRaConfig?.regionCode ?? 0)
|
||||
self.usePreset = node?.loRaConfig?.usePreset ?? true
|
||||
self.modemPreset = Int(node?.loRaConfig?.modemPreset ?? 0)
|
||||
self.txEnabled = node?.loRaConfig?.txEnabled ?? true
|
||||
self.txPower = Int(node?.loRaConfig?.txPower ?? 0)
|
||||
self.hasChanges = false
|
||||
|
||||
// Need to request a LoRaConfig from the remote node before allowing changes
|
||||
if node?.loRaConfig == nil {
|
||||
print("empty lora config")
|
||||
let adminMessageId = bleManager.requestLoRaConfig(fromUser: connectedNode!.user!, toUser: node!.user!, adminIndex: connectedNode?.myInfo?.adminIndex ?? 0)
|
||||
}
|
||||
}
|
||||
.onChange(of: region) { newRegion in
|
||||
if node != nil && node!.loRaConfig != nil {
|
||||
|
|
|
|||
|
|
@ -74,7 +74,7 @@ struct CannedMessagesConfig: View {
|
|||
|
||||
HStack {
|
||||
Label("Messages", systemImage: "message.fill")
|
||||
TextField("Messages seperate with |", text: $messages, axis: .vertical)
|
||||
TextField("Messages separate with |", text: $messages, axis: .vertical)
|
||||
.foregroundColor(.gray)
|
||||
.autocapitalization(.none)
|
||||
.disableAutocorrection(true)
|
||||
|
|
|
|||
|
|
@ -118,7 +118,7 @@ struct NetworkConfig: View {
|
|||
network.ethEnabled = self.ethEnabled
|
||||
//network.addressMode = Config.NetworkConfig.AddressMode.dhcp
|
||||
|
||||
let adminMessageId = bleManager.saveWiFiConfig(config: network, fromUser: node!.user!, toUser: node!.user!)
|
||||
let adminMessageId = bleManager.saveNetworkConfig(config: network, fromUser: node!.user!, toUser: node!.user!)
|
||||
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
|
||||
|
|
|
|||
|
|
@ -12,22 +12,87 @@ struct Settings: View {
|
|||
@Environment(\.managedObjectContext) var context
|
||||
@EnvironmentObject var bleManager: BLEManager
|
||||
@EnvironmentObject var userSettings: UserSettings
|
||||
@FetchRequest(sortDescriptors: [NSSortDescriptor(key: "lastHeard", ascending: false)], animation: .default)
|
||||
@FetchRequest(sortDescriptors: [NSSortDescriptor(key: "user.longName", ascending: true)], animation: .default)
|
||||
private var nodes: FetchedResults<NodeInfoEntity>
|
||||
@State private var selectedNode: Int = 0
|
||||
@State private var connectedNodeNum: Int = 0
|
||||
@State private var initialLoad: Bool = true
|
||||
|
||||
@State private var selection: SettingsSidebar = .about
|
||||
|
||||
enum SettingsSidebar {
|
||||
case appSettings
|
||||
case shareChannels
|
||||
case userConfig
|
||||
case loraConfig
|
||||
case channelConfig
|
||||
case bluetoothConfig
|
||||
case deviceConfig
|
||||
case displayConfig
|
||||
case networkConfig
|
||||
case positionConfig
|
||||
case cannedMessagesConfig
|
||||
case externalNotificationConfig
|
||||
case mqttConfig
|
||||
case rangeTestConfig
|
||||
case serialConfig
|
||||
case telemetryConfig
|
||||
case meshLog
|
||||
case adminMessageLog
|
||||
case about
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
NavigationSplitView {
|
||||
List {
|
||||
let connectedNodeNum = bleManager.connectedPeripheral != nil ? bleManager.connectedPeripheral.num : 0
|
||||
|
||||
NavigationLink() {
|
||||
AppSettings()
|
||||
} label: {
|
||||
|
||||
Image(systemName: "gearshape")
|
||||
.symbolRenderingMode(.hierarchical)
|
||||
Text("app.settings")
|
||||
}
|
||||
.tag(SettingsSidebar.appSettings)
|
||||
let node = nodes.first(where: { $0.num == connectedNodeNum })
|
||||
if node?.myInfo?.adminIndex ?? 0 > 0 {
|
||||
Section("Configure") {
|
||||
Picker("Configuring Node", selection: $selectedNode) {
|
||||
if connectedNodeNum == 0 {
|
||||
Text("Connect to a Node").tag(0)
|
||||
}
|
||||
ForEach(nodes) { node in
|
||||
if node.num == bleManager.connectedPeripheral?.num ?? 0 {
|
||||
Text("BLE Config: \(node.user?.longName ?? NSLocalizedString("unknown", comment: "Unknown"))")
|
||||
.tag(Int(node.num))
|
||||
} else if node.metadata != nil {
|
||||
Text("Remote Config: \(node.user?.longName ?? NSLocalizedString("unknown", comment: "Unknown"))")
|
||||
.tag(Int(node.num))
|
||||
} else {
|
||||
Text("Request Admin: \(node.user?.longName ?? NSLocalizedString("unknown", comment: "Unknown"))")
|
||||
.tag(Int(node.num))
|
||||
}
|
||||
}
|
||||
}
|
||||
.pickerStyle(.menu)
|
||||
.labelsHidden()
|
||||
.onChange(of: selectedNode) { newValue in
|
||||
// if selectedNode > 0 {
|
||||
// let node = nodes.first(where: { $0.num == newValue })
|
||||
// let connectedNode = nodes.first(where: { $0.num == connectedNodeNum })
|
||||
// connectedNodeNum = Int(bleManager.connectedPeripheral != nil ? bleManager.connectedPeripheral.num : 0)
|
||||
//
|
||||
// if node?.metadata == nil && node!.num != connectedNodeNum {
|
||||
// let adminMessageId = bleManager.requestDeviceMetadata(fromUser: connectedNode!.user!, toUser: node!.user!, adminIndex: connectedNode!.myInfo!.adminIndex, context: context)
|
||||
//
|
||||
// if adminMessageId > 0 {
|
||||
// print("Saved node metadata")
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Section("radio.configuration") {
|
||||
|
||||
NavigationLink {
|
||||
|
|
@ -37,85 +102,97 @@ struct Settings: View {
|
|||
.symbolRenderingMode(.hierarchical)
|
||||
Text("share.channels")
|
||||
}
|
||||
.tag(SettingsSidebar.shareChannels)
|
||||
.disabled(selectedNode > 0 && selectedNode != connectedNodeNum)
|
||||
|
||||
NavigationLink {
|
||||
UserConfig(node: nodes.first(where: { $0.num == connectedNodeNum }))
|
||||
UserConfig(node: nodes.first(where: { $0.num == selectedNode }), connectedNode: nodes.first(where: { $0.num == connectedNodeNum }))
|
||||
} label: {
|
||||
|
||||
Image(systemName: "person.crop.rectangle.fill")
|
||||
.symbolRenderingMode(.hierarchical)
|
||||
Text("user")
|
||||
}
|
||||
.tag(SettingsSidebar.userConfig)
|
||||
.disabled(selectedNode == 0)
|
||||
|
||||
NavigationLink() {
|
||||
|
||||
LoRaConfig(node: nodes.first(where: { $0.num == connectedNodeNum }))
|
||||
LoRaConfig(node: nodes.first(where: { $0.num == selectedNode }), connectedNode: nodes.first(where: { $0.num == connectedNodeNum }))
|
||||
} label: {
|
||||
|
||||
Image(systemName: "dot.radiowaves.left.and.right")
|
||||
.symbolRenderingMode(.hierarchical)
|
||||
Text("lora")
|
||||
}
|
||||
.tag(SettingsSidebar.loraConfig)
|
||||
.disabled(selectedNode > 0 && selectedNode != connectedNodeNum)
|
||||
|
||||
NavigationLink() {
|
||||
|
||||
Channels(node: nodes.first(where: { $0.num == connectedNodeNum }))
|
||||
} label: {
|
||||
|
||||
Image(systemName: "fibrechannel")
|
||||
.symbolRenderingMode(.hierarchical)
|
||||
|
||||
Text("channels")
|
||||
}
|
||||
.tag(SettingsSidebar.channelConfig)
|
||||
.disabled(selectedNode > 0 && selectedNode != connectedNodeNum)
|
||||
|
||||
NavigationLink() {
|
||||
|
||||
BluetoothConfig(node: nodes.first(where: { $0.num == connectedNodeNum }))
|
||||
BluetoothConfig(node: nodes.first(where: { $0.num == selectedNode }), connectedNode: nodes.first(where: { $0.num == connectedNodeNum }))
|
||||
} label: {
|
||||
Image(systemName: "antenna.radiowaves.left.and.right")
|
||||
.symbolRenderingMode(.hierarchical)
|
||||
Text("bluetooth")
|
||||
}
|
||||
.tag(SettingsSidebar.bluetoothConfig)
|
||||
.disabled(selectedNode > 0 && selectedNode != connectedNodeNum)
|
||||
|
||||
NavigationLink {
|
||||
DeviceConfig(node: nodes.first(where: { $0.num == connectedNodeNum }))
|
||||
DeviceConfig(node: nodes.first(where: { $0.num == selectedNode }))
|
||||
} label: {
|
||||
Image(systemName: "flipphone")
|
||||
.symbolRenderingMode(.hierarchical)
|
||||
Text("device")
|
||||
}
|
||||
.tag(SettingsSidebar.deviceConfig)
|
||||
.disabled(selectedNode > 0 && selectedNode != connectedNodeNum)
|
||||
|
||||
NavigationLink {
|
||||
DisplayConfig(node: nodes.first(where: { $0.num == connectedNodeNum }))
|
||||
DisplayConfig(node: nodes.first(where: { $0.num == selectedNode }))
|
||||
} label: {
|
||||
Image(systemName: "display")
|
||||
.symbolRenderingMode(.hierarchical)
|
||||
Text("display")
|
||||
}
|
||||
.tag(SettingsSidebar.displayConfig)
|
||||
.disabled(selectedNode > 0 && selectedNode != connectedNodeNum)
|
||||
|
||||
NavigationLink {
|
||||
NetworkConfig(node: nodes.first(where: { $0.num == connectedNodeNum }))
|
||||
NetworkConfig(node: nodes.first(where: { $0.num == selectedNode }))
|
||||
} label: {
|
||||
|
||||
Image(systemName: "network")
|
||||
.symbolRenderingMode(.hierarchical)
|
||||
Text("network")
|
||||
}
|
||||
.tag(SettingsSidebar.networkConfig)
|
||||
.disabled(selectedNode > 0 && selectedNode != connectedNodeNum)
|
||||
|
||||
NavigationLink {
|
||||
PositionConfig(node: nodes.first(where: { $0.num == connectedNodeNum }))
|
||||
PositionConfig(node: nodes.first(where: { $0.num == selectedNode }))
|
||||
} label: {
|
||||
|
||||
Image(systemName: "location")
|
||||
.symbolRenderingMode(.hierarchical)
|
||||
Text("position")
|
||||
}
|
||||
.tag(SettingsSidebar.positionConfig)
|
||||
.disabled(selectedNode > 0 && selectedNode != connectedNodeNum)
|
||||
|
||||
}
|
||||
Section("module.configuration") {
|
||||
|
||||
NavigationLink {
|
||||
CannedMessagesConfig(node: nodes.first(where: { $0.num == connectedNodeNum }))
|
||||
CannedMessagesConfig(node: nodes.first(where: { $0.num == selectedNode }))
|
||||
} label: {
|
||||
|
||||
Image(systemName: "list.bullet.rectangle.fill")
|
||||
|
|
@ -123,42 +200,58 @@ struct Settings: View {
|
|||
|
||||
Text("canned.messages")
|
||||
}
|
||||
.tag(SettingsSidebar.cannedMessagesConfig)
|
||||
.disabled(selectedNode > 0 && selectedNode != connectedNodeNum)
|
||||
|
||||
NavigationLink {
|
||||
ExternalNotificationConfig(node: nodes.first(where: { $0.num == connectedNodeNum }))
|
||||
ExternalNotificationConfig(node: nodes.first(where: { $0.num == selectedNode }))
|
||||
} label: {
|
||||
Image(systemName: "megaphone")
|
||||
.symbolRenderingMode(.hierarchical)
|
||||
Text("external.notification")
|
||||
}
|
||||
.tag(SettingsSidebar.externalNotificationConfig)
|
||||
.disabled(selectedNode > 0 && selectedNode != connectedNodeNum)
|
||||
|
||||
NavigationLink {
|
||||
MQTTConfig(node: nodes.first(where: { $0.num == connectedNodeNum }))
|
||||
MQTTConfig(node: nodes.first(where: { $0.num == selectedNode }))
|
||||
} label: {
|
||||
Image(systemName: "dot.radiowaves.right")
|
||||
.symbolRenderingMode(.hierarchical)
|
||||
Text("mqtt")
|
||||
}
|
||||
.tag(SettingsSidebar.mqttConfig)
|
||||
.disabled(selectedNode > 0 && selectedNode != connectedNodeNum)
|
||||
|
||||
NavigationLink {
|
||||
RangeTestConfig(node: nodes.first(where: { $0.num == connectedNodeNum }))
|
||||
RangeTestConfig(node: nodes.first(where: { $0.num == selectedNode }))
|
||||
} label: {
|
||||
Image(systemName: "point.3.connected.trianglepath.dotted")
|
||||
.symbolRenderingMode(.hierarchical)
|
||||
Text("range.test")
|
||||
}
|
||||
.tag(SettingsSidebar.rangeTestConfig)
|
||||
.disabled(selectedNode > 0 && selectedNode != connectedNodeNum)
|
||||
|
||||
NavigationLink {
|
||||
SerialConfig(node: nodes.first(where: { $0.num == connectedNodeNum }))
|
||||
SerialConfig(node: nodes.first(where: { $0.num == selectedNode }))
|
||||
} label: {
|
||||
Image(systemName: "terminal")
|
||||
.symbolRenderingMode(.hierarchical)
|
||||
Text("serial")
|
||||
}
|
||||
.tag(SettingsSidebar.serialConfig)
|
||||
.disabled(selectedNode > 0 && selectedNode != connectedNodeNum)
|
||||
|
||||
NavigationLink {
|
||||
TelemetryConfig(node: nodes.first(where: { $0.num == connectedNodeNum }))
|
||||
TelemetryConfig(node: nodes.first(where: { $0.num == selectedNode }))
|
||||
} label: {
|
||||
Image(systemName: "chart.xyaxis.line")
|
||||
.symbolRenderingMode(.hierarchical)
|
||||
Text("telemetry")
|
||||
}
|
||||
.tag(SettingsSidebar.telemetryConfig)
|
||||
.disabled(selectedNode > 0 && selectedNode != connectedNodeNum)
|
||||
}
|
||||
Section(header: Text("logging")) {
|
||||
NavigationLink {
|
||||
|
|
@ -168,6 +261,8 @@ struct Settings: View {
|
|||
.symbolRenderingMode(.hierarchical)
|
||||
Text("mesh.log")
|
||||
}
|
||||
.tag(SettingsSidebar.meshLog)
|
||||
|
||||
NavigationLink {
|
||||
let connectedNode = nodes.first(where: { $0.num == connectedNodeNum })
|
||||
AdminMessageList(user: connectedNode?.user)
|
||||
|
|
@ -176,6 +271,7 @@ struct Settings: View {
|
|||
.symbolRenderingMode(.hierarchical)
|
||||
Text("admin.log")
|
||||
}
|
||||
.tag(SettingsSidebar.adminMessageLog)
|
||||
}
|
||||
Section(header: Text("about")) {
|
||||
NavigationLink {
|
||||
|
|
@ -186,11 +282,18 @@ struct Settings: View {
|
|||
|
||||
Text("about.meshtastic")
|
||||
}
|
||||
.tag(SettingsSidebar.about)
|
||||
}
|
||||
}
|
||||
.onAppear {
|
||||
self.bleManager.context = context
|
||||
self.bleManager.userSettings = userSettings
|
||||
self.connectedNodeNum = Int(bleManager.connectedPeripheral != nil ? bleManager.connectedPeripheral.num : 0)
|
||||
if initialLoad {
|
||||
selectedNode = Int(bleManager.connectedPeripheral != nil ? bleManager.connectedPeripheral.num : 0)
|
||||
initialLoad = false
|
||||
}
|
||||
|
||||
}
|
||||
.listStyle(GroupedListStyle())
|
||||
.navigationTitle("settings")
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ struct UserConfig: View {
|
|||
@Environment(\.dismiss) private var goBack
|
||||
|
||||
var node: NodeInfoEntity?
|
||||
var connectedNode: NodeInfoEntity?
|
||||
|
||||
@State private var isPresentingFactoryResetConfirm: Bool = false
|
||||
@State private var isPresentingSaveConfirm: Bool = false
|
||||
|
|
@ -84,11 +85,11 @@ struct UserConfig: View {
|
|||
isPresented: $isPresentingSaveConfirm,
|
||||
titleVisibility: .visible
|
||||
) {
|
||||
Button("Save User Config to \(bleManager.connectedPeripheral != nil ? bleManager.connectedPeripheral.longName : "Unknown")?") {
|
||||
Button("Save User Config to \(node?.user?.longName ?? "Unknown")?") {
|
||||
var u = User()
|
||||
u.shortName = shortName
|
||||
u.longName = longName
|
||||
let adminMessageId = bleManager.saveUser(config: u, fromUser: node!.user!, toUser: node!.user!)
|
||||
let adminMessageId = bleManager.saveUser(config: u, fromUser: connectedNode!.user!, toUser: node!.user!, adminIndex: connectedNode?.myInfo?.adminIndex ?? 0)
|
||||
if adminMessageId > 0 {
|
||||
hasChanges = false
|
||||
goBack()
|
||||
|
|
|
|||
|
|
@ -92,6 +92,7 @@
|
|||
"heard"="Gehört";
|
||||
"heard.last"="Zuletzt gehört";
|
||||
"hybrid"="Hybrid";
|
||||
"hybrid.flyover"="Hybrid Flyover";
|
||||
"include"="Include";
|
||||
"inputevent.none"="Keins";
|
||||
"inputevent.up"="Hoch";
|
||||
|
|
@ -168,6 +169,7 @@
|
|||
"mesh.log.traceroute.received.route %@"="Traceroute Ergebnis: %@";
|
||||
"mesh.log.wantconfig %@"="Issuing Want Config to %@";
|
||||
"mesh.log.waypoint.sent %@"="Sent a Waypoint Packet from: %@";
|
||||
"mesh.log.waypoint.received %@"="Waypoint Packet received from node: %@";
|
||||
"message"="Nachricht";
|
||||
"message.details"="Nachrichtendetails";
|
||||
"messages"="Nachrichten";
|
||||
|
|
@ -214,6 +216,7 @@
|
|||
"routing.badRequest"="Bad Request";
|
||||
"routing.notauthorized"="Nicht authorisiert";
|
||||
"satellite"="Satellit";
|
||||
"satellite.flyover"="Satellite Flyover";
|
||||
"save"="Speichern";
|
||||
"save.config %@"="Save Config for %@";
|
||||
"serial"="Serial";
|
||||
|
|
@ -232,6 +235,7 @@
|
|||
"select.menu.item"="Wähle einen Menüeintrag aus";
|
||||
"set.region"="Setze LoRa Region";
|
||||
"standard"="Standard";
|
||||
"standard.muted"="Standard Muted";
|
||||
"ssid"="SSID";
|
||||
"tapback"="Tapback Response";
|
||||
"tapback.heart"="Gehört";
|
||||
|
|
|
|||
|
|
@ -21,7 +21,7 @@
|
|||
"battery.level.trend"="Battery Level Trend";
|
||||
"ble.name"="BLE Name";
|
||||
"ble.connection.timeout %d %@"="Connection failed after %d attempts to connect to %@. You may need to forget your device under Settings > Bluetooth.";
|
||||
"ble.errorcode.6 %@"="%@ The app will automatically reconnect to the preferred radio if it come back in range.";
|
||||
"ble.errorcode.6 %@"="%@ The app will automatically reconnect to the preferred radio if it comes back in range.";
|
||||
"ble.errorcode.14 %@"="%@ This error usually cannot be fixed without forgetting the device unders Settings > Bluetooth and re-connecting to the radio.";
|
||||
"ble.errorcode.pin %@"="%@ Please try connecting again and check the PIN carefully.";
|
||||
"bluetooth"="Bluetooth";
|
||||
|
|
@ -92,6 +92,7 @@
|
|||
"heard"="Heard";
|
||||
"heard.last"="Last Heard";
|
||||
"hybrid"="Hybrid";
|
||||
"hybrid.flyover"="Hybrid Flyover";
|
||||
"include"="Include";
|
||||
"inputevent.none"="None";
|
||||
"inputevent.up"="Up";
|
||||
|
|
@ -168,6 +169,7 @@
|
|||
"mesh.log.traceroute.sent %@"="Sent a Trace Route Request to node: %@";
|
||||
"mesh.log.wantconfig %@"="Issuing Want Config to %@";
|
||||
"mesh.log.waypoint.sent %@"="Sent a Waypoint Packet from: %@";
|
||||
"mesh.log.waypoint.received %@"="Waypoint Packet received from node: %@";
|
||||
"message"="Message";
|
||||
"message.details"="Message Details";
|
||||
"messages"="Messages";
|
||||
|
|
@ -214,6 +216,7 @@
|
|||
"routing.badRequest"="Bad Request";
|
||||
"routing.notauthorized"="Not Authorized";
|
||||
"satellite"="Satellite";
|
||||
"satellite.flyover"="Satellite Flyover";
|
||||
"save"="Save";
|
||||
"save.config %@"="Save Config for %@";
|
||||
"serial"="Serial";
|
||||
|
|
@ -232,6 +235,7 @@
|
|||
"select.menu.item"="Select an item from the menu";
|
||||
"set.region"="Set LoRa Region";
|
||||
"standard"="Standard";
|
||||
"standard.muted"="Standard Muted";
|
||||
"ssid"="SSID";
|
||||
"tapback"="Tapback Response";
|
||||
"tapback.heart"="Heart";
|
||||
|
|
|
|||
|
|
@ -1,8 +1,8 @@
|
|||
#!/bin/bash
|
||||
|
||||
# simple sanity checking for repo
|
||||
if [ ! -d "../Meshtastic-protobufs" ]; then
|
||||
echo "Please check out the https://github.com/meshtastic/Meshtastic-protobufs the parent directory."
|
||||
if [ ! -d "../protobufs/meshtastic" ]; then
|
||||
echo "Please check out the https://github.com/meshtastic/protobufs parent directory."
|
||||
exit
|
||||
fi
|
||||
|
||||
|
|
@ -12,11 +12,19 @@ if [ ! -x "`which protoc`" ]; then
|
|||
exit
|
||||
fi
|
||||
|
||||
pdir=$(realpath "../Meshtastic-protobufs")
|
||||
if [ ! -x "`which gsed`" ]; then
|
||||
echo "Please install gnu-sed by running: brew install gnu-sed"
|
||||
exit
|
||||
fi
|
||||
|
||||
pdir=$(realpath "../protobufs/meshtastic")
|
||||
sdir=$(realpath "./Meshtastic/Protobufs")
|
||||
|
||||
gsed -i 's/import "meshtastic\//import "/g' ../protobufs/meshtastic/*
|
||||
gsed -i 's/package meshtastic;//g' ../protobufs/meshtastic/*
|
||||
|
||||
echo "pdir:$pdir sdir:$sdir"
|
||||
pfiles="admin.proto apponly.proto cannedmessages.proto channel.proto config.proto device_metadata.proto deviceonly.proto localonly.proto mesh.proto module_config.proto mqtt.proto portnums.proto remote_hardware.proto
|
||||
storeforward.proto telemetry.proto"
|
||||
pfiles="admin.proto apponly.proto cannedmessages.proto channel.proto config.proto device_metadata.proto deviceonly.proto localonly.proto mesh.proto module_config.proto mqtt.proto portnums.proto remote_hardware.proto rtttl.proto storeforward.proto telemetry.proto xmodem.proto"
|
||||
for pf in $pfiles
|
||||
do
|
||||
echo "Generating $pf..."
|
||||
|
|
@ -24,3 +32,5 @@ do
|
|||
done
|
||||
echo "Done generating the swift files from the proto files."
|
||||
echo "Build, test, and commit changes."
|
||||
|
||||
cd ../protobufs/meshtastic && git reset --hard
|
||||
|
|
|
|||
261
zh-Hans.lproj/Localizable.strings
Normal file
261
zh-Hans.lproj/Localizable.strings
Normal file
|
|
@ -0,0 +1,261 @@
|
|||
/*
|
||||
Localizable.strings
|
||||
Meshtastic
|
||||
|
||||
Created by BG6TNB on 01/06/23.
|
||||
|
||||
*/
|
||||
"about"="关于";
|
||||
"about.meshtastic"="关于 Meshtastic";
|
||||
"admin"="管理员";
|
||||
"admin.log"="管理员消息日志";
|
||||
"ago"="ago";
|
||||
"airtime"="广播时间";
|
||||
"always.on"="常亮";
|
||||
"app.settings"="通用设置";
|
||||
"are.you.sure"="是否确认?";
|
||||
"ascii.capable"="ASCII Capable";
|
||||
"available.radios"="可用的电台";
|
||||
"automatic.detection"="自动识别";
|
||||
"battery.level"="电池电量";
|
||||
"battery.level.trend"="电池电量趋势";
|
||||
"ble.name"="蓝牙名称";
|
||||
"ble.connection.timeout %d %@"="尝试连接%@失败,你可能需要在系统设置的蓝牙选项中忽略该电台。";
|
||||
"ble.errorcode.6 %@"="%@ 如果在首选电台的旁边,App 将会自动重连。";
|
||||
"ble.errorcode.14 %@"="%@ 这个错误通常无法自动修复,你需要在系统设置的蓝牙选项中忽略该电台并重新配对。";
|
||||
"ble.errorcode.pin %@"="%@ 请再次尝试连接并仔细检查 PIN 码。";
|
||||
"bluetooth"="蓝牙";
|
||||
"bluetooth.off"="蓝牙已关闭";
|
||||
"bluetooth.config"="蓝牙配置";
|
||||
"bluetooth.mode.randompin"="随机 PIN 码";
|
||||
"bluetooth.mode.fixedpin"="固定 PIN 码";
|
||||
"bluetooth.mode.nopin"="不使用 PIN 码(直接配对)";
|
||||
"bluetooth.pairingmode"="配对模式";
|
||||
"bluetooth.pin.validation"="蓝牙 PIN 码必须是 6 位数字。";
|
||||
"bytes"="字节";
|
||||
"cancel"="取消";
|
||||
"canned.messages"="快捷消息";
|
||||
"canned.messages.config"="快捷消息配置";
|
||||
"canned.messages.preset.manual"="手动配置";
|
||||
"canned.messages.preset.rakrotary"="RAK 旋转编码器";
|
||||
"canned.messages.preset.cardkb"="M5 Stack 卡片键盘 / RAK 键盘";
|
||||
"channel"="频道";
|
||||
"channel.role.disabled"="禁用";
|
||||
"channel.role.primary"="主要";
|
||||
"channel.role.secondary"="次要";
|
||||
"channel.utilization"="频道利用率";
|
||||
"channels"="频道";
|
||||
"clear.app.data"="清除 App 数据";
|
||||
"clear.log"="清除日志";
|
||||
"close"="关闭";
|
||||
"config.save.confirm"="电台将会在配置保存后重启。";
|
||||
"connected.radio"="连接到电台";
|
||||
"communicating"="与电台进行通讯中...";
|
||||
"connected"="已连接到电台";
|
||||
"connecting"="连接中...";
|
||||
"contacts"="联系人";
|
||||
"copy"="复制";
|
||||
"current"="当前";
|
||||
"default"="默认";
|
||||
"delete"="删除";
|
||||
"device"="电台";
|
||||
"device.config"="电台配置";
|
||||
"device.metrics.delete"="删除所有电台指标?";
|
||||
"device.metrics.log"="电台指标日志";
|
||||
"device.role.client"="标准模式 - App 可以连接到电台进行收发操作,并且会自动转发 Mesh 网络中其他节点的消息。";
|
||||
"device.role.clientmute"="静默模式 - 与标准模式类似,App 可以连接到电台进行收发操作,但不会转发 Mesh 网络中其他节点的消息。";
|
||||
"device.role.router"="纯中继模式 - 自动转发 Mesh 网络中其他节点的消息,中继模式下屏幕会熄灭,Wi-Fi 和蓝牙将会进入睡眠模式,App 将无法连接到电台进行收发操作。";
|
||||
"device.role.routerclient"="中继模式 - 优先转发 Mesh 网络中其他节点的消息,App 也可以连接到电台进行收发操作。";
|
||||
"direct.messages"="直接收到的消息";
|
||||
"dismiss.keyboard"="隐藏键盘";
|
||||
"display"="屏幕(电台屏幕)";
|
||||
"display.config"="屏幕配置";
|
||||
"distance"="距离";
|
||||
"disconnect"="断开连接";
|
||||
"echo"="回声";
|
||||
"email.address"="邮件地址";
|
||||
"enabled"="启用";
|
||||
"encrypted"="加密";
|
||||
"external.notification"="外部通知";
|
||||
"external.notification.config"="外部通知配置";
|
||||
"firmware.version"="固件版本";
|
||||
"firmware.version.unsupported"="检测到不支持的固件版本,无法连接到电台。";
|
||||
"gas"="Gas";
|
||||
"gas.resistance"="Gas Resistance";
|
||||
"generate.qr.code"="生成二维码";
|
||||
"gpsformat.dec"="十进制";
|
||||
"gpsformat.dms"="度分秒";
|
||||
"gpsformat.utm"="通用横轴墨卡托投影";
|
||||
"gpsformat.mgrs"="军事网格参考系统";
|
||||
"gpsformat.olc"="开放的位置代码(又称加码)";
|
||||
"gpsformat.osgr"="英国国土测量局网格参考";
|
||||
"heard"="收到";
|
||||
"heard.last"="最后收到";
|
||||
"hybrid"="混合";
|
||||
"hybrid.flyover"="Hybrid Flyover";
|
||||
"include"="包含";
|
||||
"inputevent.none"="无";
|
||||
"inputevent.up"="上";
|
||||
"inputevent.down"="下";
|
||||
"inputevent.left"="左";
|
||||
"inputevent.right"="右";
|
||||
"inputevent.select"="选择";
|
||||
"inputevent.back"="后退";
|
||||
"inputevent.cancel"="取消";
|
||||
"interval.one.second"="一秒";
|
||||
"interval.two.seconds"="两秒";
|
||||
"interval.three.seconds"="三秒";
|
||||
"interval.four.seconds"="四秒";
|
||||
"interval.five.seconds"="五秒";
|
||||
"interval.ten.seconds"="十秒";
|
||||
"interval.fifteen.seconds"="十五秒";
|
||||
"interval.twenty.seconds"="二十秒";
|
||||
"interval.twentyfive.seconds"="二十五秒";
|
||||
"interval.thirty.seconds"="三十秒";
|
||||
"interval.one.minute"="一分钟";
|
||||
"interval.two.minutes"="两分钟";
|
||||
"interval.five.minutes"="五分钟";
|
||||
"interval.ten.minutes"="十分钟";
|
||||
"interval.fifteen.minutes"="十五分钟";
|
||||
"interval.thirty.minutes"="三十分钟";
|
||||
"interval.one.hour"="一小时";
|
||||
"interval.two.hours"="两小时";
|
||||
"interval.three.hours"="三小时";
|
||||
"interval.four.hours"="四小时";
|
||||
"interval.five.hours"="五小时";
|
||||
"interval.six.hours"="六小时";
|
||||
"interval.twelve.hours"="十二小时";
|
||||
"interval.eighteen.hours"="十八小时";
|
||||
"interval.twentyfour.hours"="二十四小时";
|
||||
"interval.thirtysix.hours"="三十六小时";
|
||||
"interval.tyeight.hours"="四十八小时小时";
|
||||
"interval.eventytwo.hours"="七十二小时";
|
||||
"keyboard.type"="键盘类型";
|
||||
"logging"="Logging";
|
||||
"lora"="LoRa";
|
||||
"lora.config"="LoRa 配置";
|
||||
"map"="Mesh 地图";
|
||||
"map.type"="地图类型";
|
||||
"mesh.log"="Mesh 日志";
|
||||
"mesh.log.bluetooth.config %@"="Bluetooth config received: %@";
|
||||
"mesh.log.cannedmessage.config %@"="Canned Message module config received: %@";
|
||||
"mesh.log.cannedmessages.messages.get %@"="Requested Canned Messages Module Messages for node: %@";
|
||||
"mesh.log.cannedmessages.messages.received %@"="Canned Messages Messages Received For: %@";
|
||||
"mesh.log.channel.sent %@ %d"="Sent a Channel for: %@ Channel Index %d";
|
||||
"mesh.log.channel.received %d %@"="Channel %d received from: %@";
|
||||
"mesh.log.device.config %@"="Device config received: %@";
|
||||
"mesh.log.display.config %@"="Display config received: %@";
|
||||
"mesh.log.devicemetadata %@"="Requesting Device Metadata for %@";
|
||||
"mesh.log.externalnotification.config %@"="External Notifiation module config received: %@";
|
||||
"mesh.log.lora.config %@"="LoRa config received: %@";
|
||||
"mesh.log.lora.config.sent %@"="Sent a LoRa.Config for: %@";
|
||||
"mesh.log.mqtt.config %@"="MQTT module config received: %@";
|
||||
"mesh.log.myinfo %@"="MyInfo received: %@";
|
||||
"mesh.log.network.config %@"="Network config received: %@";
|
||||
"mesh.log.nodeinfo.received %@"="Node info received for: %@";
|
||||
"mesh.log.position.config %@"="Positon config received: %@";
|
||||
"mesh.log.position.received %@"="Position Packet received from node: %@";
|
||||
"mesh.log.rangetest.config %@"="Range Test module config received: %@";
|
||||
"mesh.log.routing.message %@ %@"="Routing received for RequestID: %@ Ack Status: %@";
|
||||
"mesh.log.serial.config %@"="Serial module config received: %@";
|
||||
"mesh.log.sharelocation %@"="Sent a Position Packet from the Apple device GPS to node: %@";
|
||||
"mesh.log.telemetry.config %@"="Telemetry module config received: %@";
|
||||
"mesh.log.telemetry.received %@"="Telemetry received for: %@";
|
||||
"mesh.log.textmessage.received"="Message received from the text message app.";
|
||||
"mesh.log.textmessage.send.failed %@"="Message Send Failed, not properly connected to %@";
|
||||
"mesh.log.textmessage.sent %@ %@ %@"="Sent message %@ from %@ to %@";
|
||||
"mesh.log.traceroute.received.direct %@"="Trace Route request sent to node: %@ was recieived directly.";
|
||||
"mesh.log.traceroute.received.route %@"="Trace Route request returned: %@";
|
||||
"mesh.log.traceroute.sent %@"="Sent a Trace Route Request to node: %@";
|
||||
"mesh.log.wantconfig %@"="Issuing Want Config to %@";
|
||||
"mesh.log.waypoint.sent %@"="Sent a Waypoint Packet from: %@";
|
||||
"mesh.log.waypoint.received %@"="Waypoint Packet received from node: %@";
|
||||
"message"="消息";
|
||||
"message.details"="消息详情";
|
||||
"messages"="消息";
|
||||
"mode"="模式";
|
||||
"module.configuration"="模块配置";
|
||||
"mqtt"="MQTT";
|
||||
"mqtt.config"="MQTT 配置";
|
||||
"mqtt.username"="用户名称";
|
||||
"name"="名称";
|
||||
"network"="网络";
|
||||
"network.config"="网络配置";
|
||||
"nodes"="节点";
|
||||
"no.nodes"="未找到 Meshtastic 节点";
|
||||
"not.connected"="未连接到电台";
|
||||
"numbers.punctuation"="数字和标点符号";
|
||||
"off"="关闭";
|
||||
"on.boot"="仅在启动时";
|
||||
"options"="选项";
|
||||
"password"="密码";
|
||||
"phone.gps"="手机 GPS";
|
||||
"phone.gps.interval.description"="电台通过手机刷新定位的时间间隔,但是向 Mesh 网络中刷新定位的时间间隔由电台控制。";
|
||||
"position"="定位";
|
||||
"position.config"="定位配置";
|
||||
"preferred.radio"="首选电台";
|
||||
"provide.location"="提供定位到 Mesh 网络";
|
||||
"radio.configuration"="电台配置";
|
||||
"range.test"="拉距测试";
|
||||
"range.test.config"="拉距测试配置";
|
||||
"reply"="回复";
|
||||
"reboot"="重启";
|
||||
"reboot.node"="重启节点?";
|
||||
"received.ack"="收到确认";
|
||||
"received.ack.real"="收件人确认";
|
||||
"routing.acknowledged"="确认";
|
||||
"routing.noroute"="找不到目标";
|
||||
"routing.gotnak"="收到否认";
|
||||
"routing.timeout"="超时";
|
||||
"routing.nointerface"="无连接";
|
||||
"routing.maxretransmit"="已达到最大重试次数";
|
||||
"routing.nochannel"="没有频道";
|
||||
"routing.toolarge"="数据包过大";
|
||||
"routing.noresponse"="无响应";
|
||||
"routing.dutycyclelimit"="已达到当前区域循环周期发射上限";
|
||||
"routing.badRequest"="错误请求";
|
||||
"routing.notauthorized"="未授权";
|
||||
"satellite"="卫星";
|
||||
"satellite.flyover"="Satellite Flyover";
|
||||
"save"="保存";
|
||||
"save.config %@"="保存%@的配置";
|
||||
"serial"="串口";
|
||||
"serial.config"="串口配置";
|
||||
"serial.mode.default"="默认";
|
||||
"serial.mode.simple"="简单";
|
||||
"serial.mode.proto"="Protobufs";
|
||||
"serial.mode.txtmsg"="文本消息";
|
||||
"serial.mode.nmea"="NMEA 位置";
|
||||
"settings"="设置";
|
||||
"share.channels"="分享频道二维码";
|
||||
"share.position"="分享位置";
|
||||
"subscribed"="连接到 Mesh 网络";
|
||||
"select.contact"="选择一名联系人";
|
||||
"select.node"="选择一个节点";
|
||||
"select.menu.item"="从菜单选择一个选项";
|
||||
"set.region"="设置 LoRa 区域";
|
||||
"standard"="标准";
|
||||
"standard.muted"="Standard Muted";
|
||||
"ssid"="SSID";
|
||||
"tapback"="Tapback Response";
|
||||
"tapback.heart"="Heart";
|
||||
"tapback.thumbsup"="Thumbs Up";
|
||||
"tapback.thumbsdown"="Thumbs Down";
|
||||
"tapback.haha"="HaHa";
|
||||
"tapback.exclamation"="Exclamation Mark";
|
||||
"tapback.question"="Question Mark";
|
||||
"tapback.poop"="Poop";
|
||||
"telemetry"="遥测(传感器)";
|
||||
"telemetry.config"="遥测配置";
|
||||
"timeout"="超时";
|
||||
"timestamp"="时间戳";
|
||||
"twitter"="Twitter";
|
||||
"unknown"="未知";
|
||||
"unknown.age"="Unknown Age";
|
||||
"unset"="未设置";
|
||||
"update.firmware"="更新你的固件";
|
||||
"update.interval"="更新间隔";
|
||||
"user"="用户";
|
||||
"user.details"="用户信息";
|
||||
"voltage"="电压";
|
||||
"waiting"="等待中...";
|
||||
Loading…
Add table
Add a link
Reference in a new issue