Merge pull request #288 from meshtastic/2.0.11_Working_Changes

Merging this before it gets any worse
This commit is contained in:
Garth Vander Houwen 2023-01-26 16:30:21 -08:00 committed by GitHub
commit d6cc53397c
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
50 changed files with 3475 additions and 1779 deletions

View file

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

View file

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

View file

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

View file

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

View file

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

View 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")
// }
//}

View file

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

View file

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

View file

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

View file

@ -3,6 +3,6 @@
<plist version="1.0">
<dict>
<key>_XCCurrentVersionName</key>
<string>MeshtasticDataModelV5.xcdatamodel</string>
<string>MeshtasticDataModelV6.xcdatamodel</string>
</dict>
</plist>

View file

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

View file

@ -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 &amp;&amp; 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>

View file

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

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

View file

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

View 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" : "") }
}

View file

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

View file

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

View file

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

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

View 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"),
]
}

View file

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

View file

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

View file

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

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

View 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

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

View file

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

View file

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

View file

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

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -102,6 +102,9 @@ struct AppSettings: View {
.onAppear {
self.bleManager.context = context
}
.onChange(of: userSettings.provideLocation) { newProvideLocation in
self.bleManager.sendWantConfig()
}
}
}

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View 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"="等待中...";