diff --git a/Meshtastic.xcodeproj/project.pbxproj b/Meshtastic.xcodeproj/project.pbxproj index ea2501f9..07d7f740 100644 --- a/Meshtastic.xcodeproj/project.pbxproj +++ b/Meshtastic.xcodeproj/project.pbxproj @@ -38,7 +38,6 @@ DD4F23CD28779A3C001D37CB /* EnvironmentMetricsLog.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD4F23CC28779A3C001D37CB /* EnvironmentMetricsLog.swift */; }; DD5394FC276993AD00AD86B1 /* SwiftProtobuf in Frameworks */ = {isa = PBXBuildFile; productRef = DD5394FB276993AD00AD86B1 /* SwiftProtobuf */; }; DD5394FE276BA0EF00AD86B1 /* PositionEntityExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD5394FD276BA0EF00AD86B1 /* PositionEntityExtension.swift */; }; - DD539502276DAA6A00AD86B1 /* MapLocation.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD539501276DAA6A00AD86B1 /* MapLocation.swift */; }; DD58C5F22919AD3C00D5BEFB /* ChannelEntityExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD58C5F12919AD3C00D5BEFB /* ChannelEntityExtension.swift */; }; DD5D0A9C2931B9F200F7EA61 /* EthernetModes.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD5D0A9B2931B9F200F7EA61 /* EthernetModes.swift */; }; DD5E5202298EE33B00D21B61 /* admin.pb.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD5E51F0298EE33B00D21B61 /* admin.pb.swift */; }; @@ -50,7 +49,6 @@ DD5E5208298EE33B00D21B61 /* rtttl.pb.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD5E51F6298EE33B00D21B61 /* rtttl.pb.swift */; }; DD5E5209298EE33B00D21B61 /* module_config.pb.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD5E51F7298EE33B00D21B61 /* module_config.pb.swift */; }; DD5E520A298EE33B00D21B61 /* channel.pb.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD5E51F8298EE33B00D21B61 /* channel.pb.swift */; }; - DD5E520B298EE33B00D21B61 /* device_metadata.pb.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD5E51F9298EE33B00D21B61 /* device_metadata.pb.swift */; }; DD5E520C298EE33B00D21B61 /* portnums.pb.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD5E51FA298EE33B00D21B61 /* portnums.pb.swift */; }; DD5E520D298EE33B00D21B61 /* storeforward.pb.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD5E51FB298EE33B00D21B61 /* storeforward.pb.swift */; }; DD5E520E298EE33B00D21B61 /* mqtt.pb.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD5E51FC298EE33B00D21B61 /* mqtt.pb.swift */; }; @@ -60,7 +58,6 @@ DD5E5212298EE33B00D21B61 /* apponly.pb.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD5E5200298EE33B00D21B61 /* apponly.pb.swift */; }; DD5E5213298EE33B00D21B61 /* deviceonly.pb.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD5E5201298EE33B00D21B61 /* deviceonly.pb.swift */; }; DD5E523A298EFA5300D21B61 /* TelemetryWeather.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD5E5239298EFA5300D21B61 /* TelemetryWeather.swift */; }; - DD5E523C298F02D400D21B61 /* LicensedUser.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD5E523B298F02D400D21B61 /* LicensedUser.swift */; }; DD5E523F298F5A9E00D21B61 /* AirQualityIndexCompact.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD5E523E298F5A9E00D21B61 /* AirQualityIndexCompact.swift */; }; DD6193752862F6E600E59241 /* ExternalNotificationConfig.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD6193742862F6E600E59241 /* ExternalNotificationConfig.swift */; }; DD6193772862F90F00E59241 /* CannedMessagesConfig.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD6193762862F90F00E59241 /* CannedMessagesConfig.swift */; }; @@ -170,7 +167,6 @@ DD4A911D2708C65400501B7E /* AppSettings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppSettings.swift; sourceTree = ""; }; DD4F23CC28779A3C001D37CB /* EnvironmentMetricsLog.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EnvironmentMetricsLog.swift; sourceTree = ""; }; DD5394FD276BA0EF00AD86B1 /* PositionEntityExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PositionEntityExtension.swift; sourceTree = ""; }; - DD539501276DAA6A00AD86B1 /* MapLocation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MapLocation.swift; sourceTree = ""; }; DD58C5F12919AD3C00D5BEFB /* ChannelEntityExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChannelEntityExtension.swift; sourceTree = ""; }; DD5D0A9A2931AD6B00F7EA61 /* MeshtasticDataModelV2.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = MeshtasticDataModelV2.xcdatamodel; sourceTree = ""; }; DD5D0A9B2931B9F200F7EA61 /* EthernetModes.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EthernetModes.swift; sourceTree = ""; }; @@ -184,7 +180,6 @@ DD5E51F6298EE33B00D21B61 /* rtttl.pb.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = rtttl.pb.swift; sourceTree = ""; }; DD5E51F7298EE33B00D21B61 /* module_config.pb.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = module_config.pb.swift; sourceTree = ""; }; DD5E51F8298EE33B00D21B61 /* channel.pb.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = channel.pb.swift; sourceTree = ""; }; - DD5E51F9298EE33B00D21B61 /* device_metadata.pb.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = device_metadata.pb.swift; sourceTree = ""; }; DD5E51FA298EE33B00D21B61 /* portnums.pb.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = portnums.pb.swift; sourceTree = ""; }; DD5E51FB298EE33B00D21B61 /* storeforward.pb.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = storeforward.pb.swift; sourceTree = ""; }; DD5E51FC298EE33B00D21B61 /* mqtt.pb.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = mqtt.pb.swift; sourceTree = ""; }; @@ -194,7 +189,6 @@ DD5E5200298EE33B00D21B61 /* apponly.pb.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = apponly.pb.swift; sourceTree = ""; }; DD5E5201298EE33B00D21B61 /* deviceonly.pb.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = deviceonly.pb.swift; sourceTree = ""; }; DD5E5239298EFA5300D21B61 /* TelemetryWeather.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TelemetryWeather.swift; sourceTree = ""; }; - DD5E523B298F02D400D21B61 /* LicensedUser.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LicensedUser.swift; sourceTree = ""; }; DD5E523E298F5A9E00D21B61 /* AirQualityIndexCompact.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AirQualityIndexCompact.swift; sourceTree = ""; }; DD6193742862F6E600E59241 /* ExternalNotificationConfig.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExternalNotificationConfig.swift; sourceTree = ""; }; DD6193762862F90F00E59241 /* CannedMessagesConfig.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CannedMessagesConfig.swift; sourceTree = ""; }; @@ -239,6 +233,7 @@ DDB6ABE128B13FB500384BA1 /* PositionConfigEnums.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PositionConfigEnums.swift; sourceTree = ""; }; DDB6ABE328B13FFF00384BA1 /* DisplayEnums.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DisplayEnums.swift; sourceTree = ""; }; DDB6ABE528B1406100384BA1 /* LoraConfigEnums.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoraConfigEnums.swift; sourceTree = ""; }; + DDBA45EC299ED78100DEEDDC /* MeshtasticDataModelV8.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = MeshtasticDataModelV8.xcdatamodel; sourceTree = ""; }; DDC2E15426CE248E0042C5E4 /* Meshtastic.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Meshtastic.app; sourceTree = BUILT_PRODUCTS_DIR; }; DDC2E15726CE248E0042C5E4 /* MeshtasticApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MeshtasticApp.swift; sourceTree = ""; }; DDC2E15B26CE248F0042C5E4 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; name = Assets.xcassets; path = ../Assets.xcassets; sourceTree = ""; }; @@ -343,7 +338,6 @@ DD0F791A28713C8A00A6FDAD /* AdminMessageList.swift */, DD4A911D2708C65400501B7E /* AppSettings.swift */, DDA0B6B1294CDC55001356EC /* Channels.swift */, - DD5E523B298F02D400D21B61 /* LicensedUser.swift */, DD8169FA271F1F3A00F4AB02 /* MeshLog.swift */, DD86D40B287F401000BAEB7A /* SaveChannelQRCode.swift */, DD3501882852FC3B000FC853 /* Settings.swift */, @@ -366,7 +360,6 @@ DD5E51F6298EE33B00D21B61 /* rtttl.pb.swift */, DD5E51F7298EE33B00D21B61 /* module_config.pb.swift */, DD5E51F8298EE33B00D21B61 /* channel.pb.swift */, - DD5E51F9298EE33B00D21B61 /* device_metadata.pb.swift */, DD5E51FA298EE33B00D21B61 /* portnums.pb.swift */, DD5E51FB298EE33B00D21B61 /* storeforward.pb.swift */, DD5E51FC298EE33B00D21B61 /* mqtt.pb.swift */, @@ -548,7 +541,6 @@ isa = PBXGroup; children = ( DD23A50E26FD1B4400D9B90C /* PeripheralModel.swift */, - DD539501276DAA6A00AD86B1 /* MapLocation.swift */, DD35018A2852FC79000FC853 /* UserSettings.swift */, ); path = Model; @@ -850,7 +842,6 @@ DD8169FB271F1F3A00F4AB02 /* MeshLog.swift in Sources */, DD8ED9C52898D51F00B3B0AB /* NetworkConfig.swift in Sources */, DDC3B274283F411B00AC321C /* LastHeardText.swift in Sources */, - DD5E520B298EE33B00D21B61 /* device_metadata.pb.swift in Sources */, DD2E65262767A01F00E45FC5 /* NodeDetail.swift in Sources */, DD1925B928CDA93900720036 /* SerialConfigEnums.swift in Sources */, DD86D4112881D16900BAEB7A /* WriteCsvFile.swift in Sources */, @@ -858,7 +849,6 @@ DDA6B2E928419CF2003E8C16 /* MeshPackets.swift in Sources */, DDCE4E2C2869F92900BE9F8F /* UserConfig.swift in Sources */, DD6193752862F6E600E59241 /* ExternalNotificationConfig.swift in Sources */, - DD5E523C298F02D400D21B61 /* LicensedUser.swift in Sources */, DDB6ABE428B13FFF00384BA1 /* DisplayEnums.swift in Sources */, DD86D40A287F04F100BAEB7A /* InvalidVersion.swift in Sources */, DDD94A502845C8F5004A87A0 /* DateTimeText.swift in Sources */, @@ -879,7 +869,6 @@ DD5E5210298EE33B00D21B61 /* telemetry.pb.swift in Sources */, DD5E5205298EE33B00D21B61 /* mesh.pb.swift in Sources */, DD8169F9271F1A6100F4AB02 /* MeshLogger.swift in Sources */, - DD539502276DAA6A00AD86B1 /* MapLocation.swift in Sources */, DD41582A28585C32009B0E59 /* RangeTestConfig.swift in Sources */, DD1925B728CDA5A400720036 /* CannedMessagesConfigEnums.swift in Sources */, DD5E5211298EE33B00D21B61 /* remote_hardware.pb.swift in Sources */, @@ -1075,7 +1064,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 2.0.14; + MARKETING_VERSION = 2.0.15; PRODUCT_BUNDLE_IDENTIFIER = gvh.MeshtasticClient; PRODUCT_NAME = "$(TARGET_NAME)"; SUPPORTS_MACCATALYST = YES; @@ -1108,7 +1097,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 2.0.14; + MARKETING_VERSION = 2.0.15; PRODUCT_BUNDLE_IDENTIFIER = gvh.MeshtasticClient; PRODUCT_NAME = "$(TARGET_NAME)"; SUPPORTS_MACCATALYST = YES; @@ -1284,6 +1273,7 @@ DD3CC6BA28E366DF00FA9159 /* Meshtastic.xcdatamodeld */ = { isa = XCVersionGroup; children = ( + DDBA45EC299ED78100DEEDDC /* MeshtasticDataModelV8.xcdatamodel */, DD5E51CC2986643400D21B61 /* MeshtasticDataModelV7.xcdatamodel */, DD964FC029724F6D007C176F /* MeshtasticDataModelV6.xcdatamodel */, DD457BC4295D5E35004BCE4D /* MeshtasticDataModelV5.xcdatamodel */, @@ -1292,7 +1282,7 @@ DD5D0A9A2931AD6B00F7EA61 /* MeshtasticDataModelV2.xcdatamodel */, DD3CC6BB28E366DF00FA9159 /* MeshtasticDataModel.xcdatamodel */, ); - currentVersion = DD5E51CC2986643400D21B61 /* MeshtasticDataModelV7.xcdatamodel */; + currentVersion = DDBA45EC299ED78100DEEDDC /* MeshtasticDataModelV8.xcdatamodel */; name = Meshtastic.xcdatamodeld; path = Meshtastic/Meshtastic.xcdatamodeld; sourceTree = ""; diff --git a/Meshtastic/Enums/AppSettingsEnums.swift b/Meshtastic/Enums/AppSettingsEnums.swift index 53ce177b..7d17601d 100644 --- a/Meshtastic/Enums/AppSettingsEnums.swift +++ b/Meshtastic/Enums/AppSettingsEnums.swift @@ -35,6 +35,27 @@ enum KeyboardType: Int, CaseIterable, Identifiable { } } +enum CenteringMode: Int, CaseIterable, Identifiable { + + case allAnnotations = 0 + case allPositions = 1 + case clientGps = 2 + + var id: Int { self.rawValue } + var description: String { + get { + switch self { + case .allAnnotations: + return "All Annotations"// NSLocalizedString("default", comment: "Default Keyboard") + case .allPositions: + return "All Node Postions"// NSLocalizedString("ascii.capable", comment: "ASCII Capable Keyboard") + case .clientGps: + return "Client GPS"//NSLocalizedString("email.address", comment: "Email Address Keyboard") + } + } + } +} + enum MeshMapType: String, CaseIterable, Identifiable { case standard = "standard" diff --git a/Meshtastic/Helpers/BLEManager.swift b/Meshtastic/Helpers/BLEManager.swift index e8b47d24..167e1c0f 100644 --- a/Meshtastic/Helpers/BLEManager.swift +++ b/Meshtastic/Helpers/BLEManager.swift @@ -8,7 +8,7 @@ import MapKit // Meshtastic BLE Device Manager // --------------------------------------------------------------------------------------- class BLEManager: NSObject, CBPeripheralDelegate, ObservableObject { - + private static var documentsFolder: URL { do { return try FileManager.default.url(for: .documentDirectory, in: .userDomainMask, appropriateFor: nil, create: true) @@ -16,11 +16,12 @@ class BLEManager: NSObject, CBPeripheralDelegate, ObservableObject { fatalError("Can't find documents directory.") } } - + var context: NSManagedObjectContext? var userSettings: UserSettings? private var centralManager: CBCentralManager! - + private let restoreKey = "Meshtastic.BLE.Manager" + @Published var peripherals: [Peripheral] = [] @Published var connectedPeripheral: Peripheral! @Published var lastConnectionError: String @@ -35,35 +36,36 @@ class BLEManager: NSObject, CBPeripheralDelegate, ObservableObject { public var isConnected: Bool = false public var isSubscribed: Bool = false private var configNonce: UInt32 = 1 - + var timeoutTimer: Timer? var timeoutTimerCount = 0 var timeoutTimerRuns = 0 var positionTimer: Timer? let emptyNodeNum: UInt32 = 4294967295 - + /* Meshtastic Service Details */ var TORADIO_characteristic: CBCharacteristic! var FROMRADIO_characteristic: CBCharacteristic! var FROMNUM_characteristic: CBCharacteristic! - + let meshtasticServiceCBUUID = CBUUID(string: "0x6BA1B218-15A8-461F-9FA8-5DCAE273EAFD") let TORADIO_UUID = CBUUID(string: "0xF75C76D2-129E-4DAD-A1DD-7866124401E7") let FROMRADIO_UUID = CBUUID(string: "0x2C55E69E-4993-11ED-B878-0242AC120002") let EOL_FROMRADIO_UUID = CBUUID(string: "0x8BA2BCC2-EE02-4A55-A531-C525C5E454D5") let FROMNUM_UUID = CBUUID(string: "0xED9DA18C-A800-4F66-A670-AA7547E34453") - + //private var meshLoggingEnabled: Bool = true let meshLog = documentsFolder.appendingPathComponent("meshlog.txt") - + // MARK: init BLEManager override init() { self.lastConnectionError = "" self.connectedVersion = "0.0.0" super.init() centralManager = CBCentralManager(delegate: self, queue: nil) + //centralManager = CBCentralManager(delegate: self, queue: nil, options: [CBCentralManagerOptionRestoreIdentifierKey: restoreKey]) } - + // MARK: Scanning for BLE Devices // Scan for nearby BLE devices using the Meshtastic BLE service ID func startScanning() { @@ -130,6 +132,7 @@ class BLEManager: NSObject, CBPeripheralDelegate, ObservableObject { print("ℹ️ BLE Disconnecting from: \(connectedPeripheral.name) to connect to \(peripheral.name ?? "Unknown")") disconnectPeripheral() } + centralManager?.connect(peripheral) // Invalidate any existing timer if timeoutTimer != nil { @@ -495,6 +498,12 @@ class BLEManager: NSObject, CBPeripheralDelegate, ObservableObject { } } } + // Device Metadata + if decodedInfo.metadata.firmwareVersion.count > 0 && !invalidVersion { + nowKnown = true + deviceMetadataPacket(metadata: decodedInfo.metadata, fromNum: connectedPeripheral.num, context: context!) + } + // Log any other unknownApp calls if !nowKnown { MeshLogger.log("🕸️ MESH PACKET received for Unknown App UNHANDLED \(try! decodedInfo.packet.jsonString())") } @@ -674,7 +683,9 @@ class BLEManager: NSObject, CBPeripheralDelegate, ObservableObject { newMessage.messagePayloadMarkdown = generateMessageMarkdown(message: message) let dataType = PortNum.textMessageApp - let payloadData: Data = message.data(using: String.Encoding.utf8)! + var messageQuotesReplaced = message.replacingOccurrences(of: "’", with: "'") + messageQuotesReplaced = message.replacingOccurrences(of: "”", with: "\"") + let payloadData: Data = messageQuotesReplaced.data(using: String.Encoding.utf8)! var dataMessage = DataMessage() dataMessage.payload = payloadData @@ -1999,4 +2010,39 @@ extension BLEManager: CBCentralManagerDelegate { let visibleDuration = Calendar.current.date(byAdding: .second, value: -5, to: today)! self.peripherals.removeAll(where: { $0.lastUpdate < visibleDuration}) } + +// func centralManager(_ central: CBCentralManager, willRestoreState dict: [String : Any]) { +// +// guard let peripherals = dict[CBCentralManagerRestoredStatePeripheralsKey] as? [CBPeripheral] else { +// return +// } +// print(peripherals) +// if peripherals.count > 0 { +// //connectedPeripheral.peripheral = peripherals[0] +// // 5 +// //connectedPeripheral.peripheral.delegate = self +// +// for peripheral in peripherals { +// +// switch peripheral.state { +// case .connecting: // I've only seen this happen when +// // re-launching attached to Xcode. +// print("Xcode Restore") +// +// case .connected: // Store for connection / requesting +// // notifications when BT starts. +// print("Actual restore") +// //centralManager.connect(peripheral) +// default: break +// } +// +// +// +// // connectedPeripheral.peripheral +// //connectedPeripheral.peripheral = peripheral +// //connectedPeripheral.peripheral.delegate = self +// } +// } +// print("willRestoreState Hit!") +// } } diff --git a/Meshtastic/Helpers/Extensions.swift b/Meshtastic/Helpers/Extensions.swift index 39a2b6af..3437086e 100644 --- a/Meshtastic/Helpers/Extensions.swift +++ b/Meshtastic/Helpers/Extensions.swift @@ -72,6 +72,23 @@ extension Int { } } +extension UIImage { + func rotate(radians: Float) -> UIImage? { + var newSize = CGRect(origin: CGPoint.zero, size: self.size).applying(CGAffineTransform(rotationAngle: CGFloat(radians))).size + newSize.width = floor(newSize.width) + newSize.height = floor(newSize.height) + UIGraphicsBeginImageContextWithOptions(newSize, false, self.scale) + let context = UIGraphicsGetCurrentContext()! + context.translateBy(x: newSize.width/2, y: newSize.height/2) + context.rotate(by: CGFloat(radians)) + self.draw(in: CGRect(x: -self.size.width/2, y: -self.size.height/2, width: self.size.width, height: self.size.height)) + let newImage = UIGraphicsGetImageFromCurrentImageContext() + UIGraphicsEndImageContext() + + return newImage + } +} + extension String { func base64urlToBase64() -> String { diff --git a/Meshtastic/Meshtastic.xcdatamodeld/.xccurrentversion b/Meshtastic/Meshtastic.xcdatamodeld/.xccurrentversion index aab08b69..25c323b3 100644 --- a/Meshtastic/Meshtastic.xcdatamodeld/.xccurrentversion +++ b/Meshtastic/Meshtastic.xcdatamodeld/.xccurrentversion @@ -3,6 +3,6 @@ _XCCurrentVersionName - MeshtasticDataModelV7.xcdatamodel + MeshtasticDataModelV8.xcdatamodel diff --git a/Meshtastic/Meshtastic.xcdatamodeld/MeshtasticDataModelV7.xcdatamodel/contents b/Meshtastic/Meshtastic.xcdatamodeld/MeshtasticDataModelV7.xcdatamodel/contents index 35e5b8ef..cae65994 100644 --- a/Meshtastic/Meshtastic.xcdatamodeld/MeshtasticDataModelV7.xcdatamodel/contents +++ b/Meshtastic/Meshtastic.xcdatamodeld/MeshtasticDataModelV7.xcdatamodel/contents @@ -1,5 +1,5 @@ - + diff --git a/Meshtastic/Meshtastic.xcdatamodeld/MeshtasticDataModelV8.xcdatamodel/contents b/Meshtastic/Meshtastic.xcdatamodeld/MeshtasticDataModelV8.xcdatamodel/contents new file mode 100644 index 00000000..a3184912 --- /dev/null +++ b/Meshtastic/Meshtastic.xcdatamodeld/MeshtasticDataModelV8.xcdatamodel/contents @@ -0,0 +1,301 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/Meshtastic/Model/MapLocation.swift b/Meshtastic/Model/MapLocation.swift deleted file mode 100644 index 77fd9d02..00000000 --- a/Meshtastic/Model/MapLocation.swift +++ /dev/null @@ -1,15 +0,0 @@ -// -// MapLocation.swift -// MeshtasticApple -// -// Created by Garth Vander Houwen on 12/17/21. -// -import Foundation -import MapKit - -struct MapLocation: Identifiable { - - let id = UUID() - let name: String - let coordinate: CLLocationCoordinate2D -} diff --git a/Meshtastic/Model/UserSettings.swift b/Meshtastic/Model/UserSettings.swift index 2b95b871..675d238c 100644 --- a/Meshtastic/Model/UserSettings.swift +++ b/Meshtastic/Model/UserSettings.swift @@ -45,6 +45,18 @@ class UserSettings: ObservableObject { UserDefaults.standard.set(meshMapType, forKey: "meshMapType") } } + @Published var meshMapCenteringMode: Int { + didSet { + UserDefaults.standard.set(meshMapCenteringMode, forKey: "meshMapCenteringMode") + UserDefaults.standard.synchronize() + } + } + @Published var meshMapRecentering: Bool { + didSet { + UserDefaults.standard.set(meshMapCenteringMode, forKey: "meshMapRecentering") + UserDefaults.standard.synchronize() + } + } @Published var meshMapCustomTileServer: String { didSet { UserDefaults.standard.set(meshMapCustomTileServer, forKey: "meshMapCustomTileServer") @@ -59,7 +71,9 @@ class UserSettings: ObservableObject { self.provideLocation = UserDefaults.standard.object(forKey: "provideLocation") as? Bool ?? false self.provideLocationInterval = UserDefaults.standard.object(forKey: "provideLocationInterval") as? Int ?? 900 self.keyboardType = UserDefaults.standard.object(forKey: "keyboardType") as? Int ?? 0 - self.meshMapType = UserDefaults.standard.string(forKey: "meshMapType") ?? "hybrid" + self.meshMapType = UserDefaults.standard.string(forKey: "meshMapType") ?? "standard" + self.meshMapCenteringMode = UserDefaults.standard.object(forKey: "meshMapCenteringMode") as? Int ?? 0 + self.meshMapRecentering = UserDefaults.standard.object(forKey: "meshMapRecentering") as? Bool ?? true self.meshMapCustomTileServer = UserDefaults.standard.string(forKey: "meshMapCustomTileServer") ?? "" } } diff --git a/Meshtastic/Persistence/PositionEntityExtension.swift b/Meshtastic/Persistence/PositionEntityExtension.swift index 6bda7147..23ec54b2 100644 --- a/Meshtastic/Persistence/PositionEntityExtension.swift +++ b/Meshtastic/Persistence/PositionEntityExtension.swift @@ -52,6 +52,6 @@ extension PositionEntity { extension PositionEntity: MKAnnotation { public var coordinate: CLLocationCoordinate2D { nodeCoordinate ?? LocationHelper.DefaultLocation } - public var title: String? { nodePosition?.user?.longName ?? NSLocalizedString("unknown", comment: "Unknown") } + public var title: String? { nodePosition?.user?.shortName ?? NSLocalizedString("unknown", comment: "Unknown") } public var subtitle: String? { time?.formatted() } } diff --git a/Meshtastic/Persistence/UpdateCoreData.swift b/Meshtastic/Persistence/UpdateCoreData.swift index 4e37461c..e04170a6 100644 --- a/Meshtastic/Persistence/UpdateCoreData.swift +++ b/Meshtastic/Persistence/UpdateCoreData.swift @@ -119,7 +119,18 @@ func upsertPositionPacket (packet: MeshPacket, context: NSManagedObjectContext) let fetchedNode = try context.fetch(fetchNodePositionRequest) as! [NodeInfoEntity] if fetchedNode.count == 1 { + // Unset the current latest position for this node + let fetchCurrentLatestPositionsRequest: NSFetchRequest = NSFetchRequest.init(entityName: "PositionEntity") + fetchCurrentLatestPositionsRequest.predicate = NSPredicate(format: "nodePosition.num == %lld && latest = true", Int64(packet.from)) + let fetchedPositions = try context.fetch(fetchCurrentLatestPositionsRequest) as! [PositionEntity] + if fetchedPositions.count > 0 { + for position in fetchedPositions { + position.latest = false + } + } + let position = PositionEntity(context: context) + position.latest = true position.snr = packet.rxSnr position.seqNo = Int32(positionMessage.seqNumber) position.latitudeI = positionMessage.latitudeI @@ -134,12 +145,14 @@ func upsertPositionPacket (packet: MeshPacket, context: NSManagedObjectContext) 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)") diff --git a/Meshtastic/Protobufs/meshtastic/admin.pb.swift b/Meshtastic/Protobufs/meshtastic/admin.pb.swift index 1cda1982..eb4aafba 100644 --- a/Meshtastic/Protobufs/meshtastic/admin.pb.swift +++ b/Meshtastic/Protobufs/meshtastic/admin.pb.swift @@ -794,6 +794,10 @@ struct HamParameters { /// Ensure your radio is capable of operating of the selected frequency before setting this. var frequency: Float = 0 + /// + /// Optional short name of user + var shortName: String = String() + var unknownFields = SwiftProtobuf.UnknownStorage() init() {} @@ -1335,6 +1339,7 @@ extension HamParameters: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementa 1: .standard(proto: "call_sign"), 2: .standard(proto: "tx_power"), 3: .same(proto: "frequency"), + 4: .standard(proto: "short_name"), ] mutating func decodeMessage(decoder: inout D) throws { @@ -1346,6 +1351,7 @@ extension HamParameters: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementa case 1: try { try decoder.decodeSingularStringField(value: &self.callSign) }() case 2: try { try decoder.decodeSingularInt32Field(value: &self.txPower) }() case 3: try { try decoder.decodeSingularFloatField(value: &self.frequency) }() + case 4: try { try decoder.decodeSingularStringField(value: &self.shortName) }() default: break } } @@ -1361,6 +1367,9 @@ extension HamParameters: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementa if self.frequency != 0 { try visitor.visitSingularFloatField(value: self.frequency, fieldNumber: 3) } + if !self.shortName.isEmpty { + try visitor.visitSingularStringField(value: self.shortName, fieldNumber: 4) + } try unknownFields.traverse(visitor: &visitor) } @@ -1368,6 +1377,7 @@ extension HamParameters: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementa if lhs.callSign != rhs.callSign {return false} if lhs.txPower != rhs.txPower {return false} if lhs.frequency != rhs.frequency {return false} + if lhs.shortName != rhs.shortName {return false} if lhs.unknownFields != rhs.unknownFields {return false} return true } diff --git a/Meshtastic/Protobufs/meshtastic/config.pb.swift b/Meshtastic/Protobufs/meshtastic/config.pb.swift index 0013ae82..c6bfb474 100644 --- a/Meshtastic/Protobufs/meshtastic/config.pb.swift +++ b/Meshtastic/Protobufs/meshtastic/config.pb.swift @@ -172,6 +172,11 @@ struct Config { /// Sets the role of node var rebroadcastMode: Config.DeviceConfig.RebroadcastMode = .all + /// + /// Send our nodeinfo this often + /// Defaults to 900 Seconds (15 minutes) + var nodeInfoBroadcastSecs: UInt32 = 0 + var unknownFields = SwiftProtobuf.UnknownStorage() /// @@ -1561,6 +1566,7 @@ extension Config.DeviceConfig: SwiftProtobuf.Message, SwiftProtobuf._MessageImpl 4: .standard(proto: "button_gpio"), 5: .standard(proto: "buzzer_gpio"), 6: .standard(proto: "rebroadcast_mode"), + 7: .standard(proto: "node_info_broadcast_secs"), ] mutating func decodeMessage(decoder: inout D) throws { @@ -1575,6 +1581,7 @@ extension Config.DeviceConfig: SwiftProtobuf.Message, SwiftProtobuf._MessageImpl case 4: try { try decoder.decodeSingularUInt32Field(value: &self.buttonGpio) }() case 5: try { try decoder.decodeSingularUInt32Field(value: &self.buzzerGpio) }() case 6: try { try decoder.decodeSingularEnumField(value: &self.rebroadcastMode) }() + case 7: try { try decoder.decodeSingularUInt32Field(value: &self.nodeInfoBroadcastSecs) }() default: break } } @@ -1599,6 +1606,9 @@ extension Config.DeviceConfig: SwiftProtobuf.Message, SwiftProtobuf._MessageImpl if self.rebroadcastMode != .all { try visitor.visitSingularEnumField(value: self.rebroadcastMode, fieldNumber: 6) } + if self.nodeInfoBroadcastSecs != 0 { + try visitor.visitSingularUInt32Field(value: self.nodeInfoBroadcastSecs, fieldNumber: 7) + } try unknownFields.traverse(visitor: &visitor) } @@ -1609,6 +1619,7 @@ extension Config.DeviceConfig: SwiftProtobuf.Message, SwiftProtobuf._MessageImpl if lhs.buttonGpio != rhs.buttonGpio {return false} if lhs.buzzerGpio != rhs.buzzerGpio {return false} if lhs.rebroadcastMode != rhs.rebroadcastMode {return false} + if lhs.nodeInfoBroadcastSecs != rhs.nodeInfoBroadcastSecs {return false} if lhs.unknownFields != rhs.unknownFields {return false} return true } diff --git a/Meshtastic/Protobufs/meshtastic/device_metadata.pb.swift b/Meshtastic/Protobufs/meshtastic/device_metadata.pb.swift deleted file mode 100644 index 4c930c43..00000000 --- a/Meshtastic/Protobufs/meshtastic/device_metadata.pb.swift +++ /dev/null @@ -1,157 +0,0 @@ -// DO NOT EDIT. -// swift-format-ignore-file -// -// Generated by the Swift generator plugin for the protocol buffer compiler. -// Source: meshtastic/device_metadata.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 -} - -/// -/// Device metadata response -struct DeviceMetadata { - // 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. - - /// - /// Device firmware version string - var firmwareVersion: String = String() - - /// - /// Device state version - var deviceStateVersion: UInt32 = 0 - - /// - /// Indicates whether the device can shutdown CPU natively or via power management chip - var canShutdown: Bool = false - - /// - /// Indicates that the device has native wifi capability - var hasWifi_p: Bool = false - - /// - /// Indicates that the device has native bluetooth capability - var hasBluetooth_p: Bool = false - - /// - /// 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 - - /// - /// Device hardware model - var hwModel: HardwareModel = .unset - - var unknownFields = SwiftProtobuf.UnknownStorage() - - init() {} -} - -#if swift(>=5.5) && canImport(_Concurrency) -extension DeviceMetadata: @unchecked Sendable {} -#endif // swift(>=5.5) && canImport(_Concurrency) - -// MARK: - Code below here is support for the SwiftProtobuf runtime. - -fileprivate let _protobuf_package = "meshtastic" - -extension DeviceMetadata: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { - static let protoMessageName: String = _protobuf_package + ".DeviceMetadata" - static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ - 1: .standard(proto: "firmware_version"), - 2: .standard(proto: "device_state_version"), - 3: .same(proto: "canShutdown"), - 4: .same(proto: "hasWifi"), - 5: .same(proto: "hasBluetooth"), - 6: .same(proto: "hasEthernet"), - 7: .same(proto: "role"), - 8: .standard(proto: "position_flags"), - 9: .standard(proto: "hw_model"), - ] - - mutating func decodeMessage(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.firmwareVersion) }() - case 2: try { try decoder.decodeSingularUInt32Field(value: &self.deviceStateVersion) }() - case 3: try { try decoder.decodeSingularBoolField(value: &self.canShutdown) }() - 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) }() - case 9: try { try decoder.decodeSingularEnumField(value: &self.hwModel) }() - default: break - } - } - } - - func traverse(visitor: inout V) throws { - if !self.firmwareVersion.isEmpty { - try visitor.visitSingularStringField(value: self.firmwareVersion, fieldNumber: 1) - } - if self.deviceStateVersion != 0 { - try visitor.visitSingularUInt32Field(value: self.deviceStateVersion, fieldNumber: 2) - } - if self.canShutdown != false { - try visitor.visitSingularBoolField(value: self.canShutdown, fieldNumber: 3) - } - if self.hasWifi_p != false { - try visitor.visitSingularBoolField(value: self.hasWifi_p, fieldNumber: 4) - } - if self.hasBluetooth_p != false { - try visitor.visitSingularBoolField(value: self.hasBluetooth_p, fieldNumber: 5) - } - 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) - } - if self.hwModel != .unset { - try visitor.visitSingularEnumField(value: self.hwModel, fieldNumber: 9) - } - try unknownFields.traverse(visitor: &visitor) - } - - static func ==(lhs: DeviceMetadata, rhs: DeviceMetadata) -> Bool { - if lhs.firmwareVersion != rhs.firmwareVersion {return false} - if lhs.deviceStateVersion != rhs.deviceStateVersion {return false} - if lhs.canShutdown != rhs.canShutdown {return false} - 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.hwModel != rhs.hwModel {return false} - if lhs.unknownFields != rhs.unknownFields {return false} - return true - } -} diff --git a/Meshtastic/Protobufs/meshtastic/mesh.pb.swift b/Meshtastic/Protobufs/meshtastic/mesh.pb.swift index 0f474574..12b53077 100644 --- a/Meshtastic/Protobufs/meshtastic/mesh.pb.swift +++ b/Meshtastic/Protobufs/meshtastic/mesh.pb.swift @@ -158,6 +158,10 @@ enum HardwareModel: SwiftProtobuf.Enum { /// New BETAFPV ELRS Micro TX Module 2.4G with ESP32 CPU case betafpv2400Tx // = 45 + /// + /// BetaFPV ExpressLRS "Nano" TX Module 900MHz with ESP32 CPU + case betafpv900NanoTx // = 46 + /// /// Reserved ID For developing private Ports. These will show up in live traffic sparsely, so we can use a high number. Keep it within 8 bits. case privateHw // = 255 @@ -201,6 +205,7 @@ enum HardwareModel: SwiftProtobuf.Enum { case 43: self = .heltecV3 case 44: self = .heltecWslV3 case 45: self = .betafpv2400Tx + case 46: self = .betafpv900NanoTx case 255: self = .privateHw default: self = .UNRECOGNIZED(rawValue) } @@ -240,6 +245,7 @@ enum HardwareModel: SwiftProtobuf.Enum { case .heltecV3: return 43 case .heltecWslV3: return 44 case .betafpv2400Tx: return 45 + case .betafpv900NanoTx: return 46 case .privateHw: return 255 case .UNRECOGNIZED(let i): return i } @@ -284,6 +290,7 @@ extension HardwareModel: CaseIterable { .heltecV3, .heltecWslV3, .betafpv2400Tx, + .betafpv900NanoTx, .privateHw, ] } @@ -1949,6 +1956,16 @@ struct FromRadio { set {_uniqueStorage()._payloadVariant = .xmodemPacket(newValue)} } + /// + /// Device metadata message + var metadata: DeviceMetadata { + get { + if case .metadata(let v)? = _storage._payloadVariant {return v} + return DeviceMetadata() + } + set {_uniqueStorage()._payloadVariant = .metadata(newValue)} + } + var unknownFields = SwiftProtobuf.UnknownStorage() /// @@ -1995,6 +2012,9 @@ struct FromRadio { /// /// File Transfer Chunk case xmodemPacket(XModem) + /// + /// Device metadata message + case metadata(DeviceMetadata) #if !swift(>=4.1) static func ==(lhs: FromRadio.OneOf_PayloadVariant, rhs: FromRadio.OneOf_PayloadVariant) -> Bool { @@ -2046,6 +2066,10 @@ struct FromRadio { guard case .xmodemPacket(let l) = lhs, case .xmodemPacket(let r) = rhs else { preconditionFailure() } return l == r }() + case (.metadata, .metadata): return { + guard case .metadata(let l) = lhs, case .metadata(let r) = rhs else { preconditionFailure() } + return l == r + }() default: return false } } @@ -2192,6 +2216,54 @@ struct Compressed { init() {} } +/// +/// Device metadata response +struct DeviceMetadata { + // 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. + + /// + /// Device firmware version string + var firmwareVersion: String = String() + + /// + /// Device state version + var deviceStateVersion: UInt32 = 0 + + /// + /// Indicates whether the device can shutdown CPU natively or via power management chip + var canShutdown: Bool = false + + /// + /// Indicates that the device has native wifi capability + var hasWifi_p: Bool = false + + /// + /// Indicates that the device has native bluetooth capability + var hasBluetooth_p: Bool = false + + /// + /// 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 + + /// + /// Device hardware model + var hwModel: HardwareModel = .unset + + var unknownFields = SwiftProtobuf.UnknownStorage() + + init() {} +} + #if swift(>=5.5) && canImport(_Concurrency) extension HardwareModel: @unchecked Sendable {} extension Constants: @unchecked Sendable {} @@ -2220,6 +2292,7 @@ extension FromRadio.OneOf_PayloadVariant: @unchecked Sendable {} extension ToRadio: @unchecked Sendable {} extension ToRadio.OneOf_PayloadVariant: @unchecked Sendable {} extension Compressed: @unchecked Sendable {} +extension DeviceMetadata: @unchecked Sendable {} #endif // swift(>=5.5) && canImport(_Concurrency) // MARK: - Code below here is support for the SwiftProtobuf runtime. @@ -2260,6 +2333,7 @@ extension HardwareModel: SwiftProtobuf._ProtoNameProviding { 43: .same(proto: "HELTEC_V3"), 44: .same(proto: "HELTEC_WSL_V3"), 45: .same(proto: "BETAFPV_2400_TX"), + 46: .same(proto: "BETAFPV_900_NANO_TX"), 255: .same(proto: "PRIVATE_HW"), ] } @@ -3401,6 +3475,7 @@ extension FromRadio: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementation 10: .same(proto: "channel"), 11: .same(proto: "queueStatus"), 12: .same(proto: "xmodemPacket"), + 13: .same(proto: "metadata"), ] fileprivate class _StorageClass { @@ -3566,6 +3641,19 @@ extension FromRadio: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementation _storage._payloadVariant = .xmodemPacket(v) } }() + case 13: try { + var v: DeviceMetadata? + var hadOneofValue = false + if let current = _storage._payloadVariant { + hadOneofValue = true + if case .metadata(let m) = current {v = m} + } + try decoder.decodeSingularMessageField(value: &v) + if let v = v { + if hadOneofValue {try decoder.handleConflictingOneOf()} + _storage._payloadVariant = .metadata(v) + } + }() default: break } } @@ -3626,6 +3714,10 @@ extension FromRadio: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementation guard case .xmodemPacket(let v)? = _storage._payloadVariant else { preconditionFailure() } try visitor.visitSingularMessageField(value: v, fieldNumber: 12) }() + case .metadata?: try { + guard case .metadata(let v)? = _storage._payloadVariant else { preconditionFailure() } + try visitor.visitSingularMessageField(value: v, fieldNumber: 13) + }() case nil: break } } @@ -3781,3 +3873,83 @@ extension Compressed: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementatio return true } } + +extension DeviceMetadata: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { + static let protoMessageName: String = _protobuf_package + ".DeviceMetadata" + static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ + 1: .standard(proto: "firmware_version"), + 2: .standard(proto: "device_state_version"), + 3: .same(proto: "canShutdown"), + 4: .same(proto: "hasWifi"), + 5: .same(proto: "hasBluetooth"), + 6: .same(proto: "hasEthernet"), + 7: .same(proto: "role"), + 8: .standard(proto: "position_flags"), + 9: .standard(proto: "hw_model"), + ] + + mutating func decodeMessage(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.firmwareVersion) }() + case 2: try { try decoder.decodeSingularUInt32Field(value: &self.deviceStateVersion) }() + case 3: try { try decoder.decodeSingularBoolField(value: &self.canShutdown) }() + 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) }() + case 9: try { try decoder.decodeSingularEnumField(value: &self.hwModel) }() + default: break + } + } + } + + func traverse(visitor: inout V) throws { + if !self.firmwareVersion.isEmpty { + try visitor.visitSingularStringField(value: self.firmwareVersion, fieldNumber: 1) + } + if self.deviceStateVersion != 0 { + try visitor.visitSingularUInt32Field(value: self.deviceStateVersion, fieldNumber: 2) + } + if self.canShutdown != false { + try visitor.visitSingularBoolField(value: self.canShutdown, fieldNumber: 3) + } + if self.hasWifi_p != false { + try visitor.visitSingularBoolField(value: self.hasWifi_p, fieldNumber: 4) + } + if self.hasBluetooth_p != false { + try visitor.visitSingularBoolField(value: self.hasBluetooth_p, fieldNumber: 5) + } + 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) + } + if self.hwModel != .unset { + try visitor.visitSingularEnumField(value: self.hwModel, fieldNumber: 9) + } + try unknownFields.traverse(visitor: &visitor) + } + + static func ==(lhs: DeviceMetadata, rhs: DeviceMetadata) -> Bool { + if lhs.firmwareVersion != rhs.firmwareVersion {return false} + if lhs.deviceStateVersion != rhs.deviceStateVersion {return false} + if lhs.canShutdown != rhs.canShutdown {return false} + 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.hwModel != rhs.hwModel {return false} + if lhs.unknownFields != rhs.unknownFields {return false} + return true + } +} diff --git a/Meshtastic/Views/Bluetooth/Connect.swift b/Meshtastic/Views/Bluetooth/Connect.swift index d9e29e1b..0f7affb4 100644 --- a/Meshtastic/Views/Bluetooth/Connect.swift +++ b/Meshtastic/Views/Bluetooth/Connect.swift @@ -22,10 +22,10 @@ struct Connect: View { @State var isUnsetRegion = false @State var invalidFirmwareVersion = false - var body: some View { + var body: some View { NavigationStack { - VStack { + VStack { List { if bleManager.isSwitchedOn { Section(header: Text("connected.radio").font(.title)) { @@ -202,7 +202,7 @@ struct Connect: View { HStack(alignment: .center) { Spacer() - + #if targetEnvironment(macCatalyst) if bleManager.connectedPeripheral != nil { @@ -226,15 +226,15 @@ struct Connect: View { } #endif Spacer() - } + } .padding(.bottom, 10) - } - .navigationTitle("bluetooth") + } + .navigationTitle("bluetooth") .navigationBarItems(leading: MeshtasticLogo(), trailing: ZStack { ConnectedDevice(bluetoothOn: bleManager.isSwitchedOn, deviceConnected: bleManager.connectedPeripheral != nil, name: (bleManager.connectedPeripheral != nil) ? bleManager.connectedPeripheral.shortName : "????") }) - } + } .sheet(isPresented: $invalidFirmwareVersion, onDismiss: didDismissSheet) { InvalidVersion(minimumVersion: self.bleManager.minimumVersion, version: self.bleManager.connectedVersion) .presentationDetents([.large]) @@ -267,7 +267,7 @@ struct Connect: View { } } } - .onAppear(perform: { + .onAppear(perform: { self.bleManager.context = context self.bleManager.userSettings = userSettings @@ -285,7 +285,7 @@ struct Connect: View { isPreferredRadio = false } }) - } + } func didDismissSheet() { bleManager.disconnectPeripheral(reconnect: false) } diff --git a/Meshtastic/Views/ContentView.swift b/Meshtastic/Views/ContentView.swift index a3f99164..534062c0 100644 --- a/Meshtastic/Views/ContentView.swift +++ b/Meshtastic/Views/ContentView.swift @@ -25,7 +25,7 @@ struct ContentView: View { TabView(selection: $selection) { - if userSettings.preferredNodeNum > 0 { + if userSettings.preferredPeripheralId.count > 0 { Contacts() .tabItem { diff --git a/Meshtastic/Views/Map/Custom/MapViewSwiftUI.swift b/Meshtastic/Views/Map/Custom/MapViewSwiftUI.swift index 70e8a9e0..4cc5ef0b 100644 --- a/Meshtastic/Views/Map/Custom/MapViewSwiftUI.swift +++ b/Meshtastic/Views/Map/Custom/MapViewSwiftUI.swift @@ -7,6 +7,10 @@ import SwiftUI import MapKit +func degreesToRadians(_ number: Double) -> Double { + return number * .pi / 180 +} + struct MapViewSwiftUI: UIViewRepresentable { var onLongPress: (_ waypointCoordinate: CLLocationCoordinate2D) -> Void @@ -15,7 +19,10 @@ struct MapViewSwiftUI: UIViewRepresentable { let positions: [PositionEntity] let waypoints: [WaypointEntity] let mapViewType: MKMapType + let centeringMode: CenteringMode + let centerOnPositionsOnly: Bool + @AppStorage("meshMapRecentering") private var recenter = false // Offline Maps //make this view dependent on the UserDefault that is updated when importing a new map file @@ -28,28 +35,54 @@ struct MapViewSwiftUI: UIViewRepresentable { func makeUIView(context: Context) -> MKMapView { // Parameters + mapView.mapType = mapViewType mapView.addAnnotations(waypoints) - if centerOnPositionsOnly { - mapView.fit(annotations: positions, andShow: true) - } else { + // Logic to manage the map centering options + switch centeringMode { + case .allAnnotations: mapView.addAnnotations(positions) mapView.fitAllAnnotations() + case .allPositions: + mapView.fit(annotations: positions, andShow: true) + case .clientGps: + + let span = MKCoordinateSpan(latitudeDelta: 0.003, longitudeDelta: 0.003) + let center = CLLocationCoordinate2D(latitude: LocationHelper.currentLocation.latitude, longitude: LocationHelper.currentLocation.longitude) + let region = MKCoordinateRegion(center: center, span: span) + mapView.setRegion(region, animated: true) + mapView.setUserTrackingMode(.followWithHeading, animated: true) + mapView.addAnnotations(positions) } - mapView.mapType = mapViewType - mapView.setUserTrackingMode(.none, animated: true) + // Other MKMapView Settings + mapView.showsUserLocation = true + mapView.preferredConfiguration.elevationStyle = .realistic 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) + +#if targetEnvironment(macCatalyst) + // Show the default always visible compass and the mac only controls + mapView.showsCompass = true mapView.showsZoomControls = true - #endif + mapView.showsPitchControl = true +#else + +#if os(iOS) + // Hide the default compass that only appears when you are not going north and instead always show the compass in the bottom right corner of the map + mapView.showsCompass = false + let compassButton = MKCompassButton(mapView: mapView) // Make a new compass + compassButton.compassVisibility = .visible // Make it visible + mapView.addSubview(compassButton) // Add it to the view + compassButton.translatesAutoresizingMaskIntoConstraints = false + compassButton.trailingAnchor.constraint(equalTo: mapView.trailingAnchor, constant: -5).isActive = true + compassButton.bottomAnchor.constraint(equalTo: mapView.bottomAnchor, constant: -25).isActive = true +#endif +#endif mapView.delegate = context.coordinator return mapView } @@ -82,8 +115,27 @@ struct MapViewSwiftUI: UIViewRepresentable { DispatchQueue.main.async { mapView.removeAnnotations(mapView.annotations) - mapView.addAnnotations(positions) mapView.addAnnotations(waypoints) + switch centeringMode { + case .allAnnotations: + mapView.addAnnotations(positions) + if recenter { + mapView.fitAllAnnotations() + } + case .allPositions: + if recenter { + mapView.fit(annotations: positions, andShow: true) + } else { + mapView.addAnnotations(positions) + } + case .clientGps: + mapView.addAnnotations(positions) + if recenter { + let span = MKCoordinateSpan(latitudeDelta: 0.003, longitudeDelta: 0.003) + let region = MKCoordinateRegion(center: CLLocationCoordinate2D(latitude: LocationHelper.currentLocation.latitude, longitude: LocationHelper.currentLocation.longitude), span: span) + mapView.setRegion(region, animated: true) + } + } } } @@ -114,20 +166,18 @@ struct MapViewSwiftUI: UIViewRepresentable { switch annotation { case _ as MKClusterAnnotation: - let annotationView = mapView.dequeueReusableAnnotationView(withIdentifier: "nodeGroup") as? MKMarkerAnnotationView ?? MKMarkerAnnotationView(annotation: annotation, reuseIdentifier: "NodeGroup") + let annotationView = mapView.dequeueReusableAnnotationView(withIdentifier: "nodeGroup") as? MKMarkerAnnotationView ?? MKMarkerAnnotationView(annotation: annotation, reuseIdentifier: "WaypointGroup") annotationView.markerTintColor = .brown//.systemRed annotationView.displayPriority = .defaultLow annotationView.tag = -1 return annotationView case let positionAnnotation as PositionEntity: - let annotationView = mapView.dequeueReusableAnnotationView(withIdentifier: "node") as? MKMarkerAnnotationView ?? MKMarkerAnnotationView(annotation: annotation, reuseIdentifier: "Node") + let reuseID = String(positionAnnotation.nodePosition?.num ?? 0) + "-" + String(positionAnnotation.time?.timeIntervalSince1970 ?? 0) + let annotationView = mapView.dequeueReusableAnnotationView(withIdentifier: "node") as? MKMarkerAnnotationView ?? MKMarkerAnnotationView(annotation: annotation, reuseIdentifier: reuseID ) annotationView.tag = -1 annotationView.canShowCallout = true - annotationView.glyphText = "📟" - let latest = parent.positions.last(where: { $0.nodePosition?.num ?? 0 == positionAnnotation.nodePosition?.num ?? -1 }) - - if latest == positionAnnotation { + if positionAnnotation.latest { annotationView.markerTintColor = .systemRed annotationView.displayPriority = .required annotationView.titleVisibility = .visible @@ -136,19 +186,33 @@ struct MapViewSwiftUI: UIViewRepresentable { annotationView.markerTintColor = UIColor(.indigo) annotationView.displayPriority = .defaultHigh annotationView.titleVisibility = .adaptive - annotationView.clusteringIdentifier = "nodeGroup" } - + annotationView.tag = -1 + annotationView.canShowCallout = true annotationView.titleVisibility = .adaptive let leftIcon = UIImageView(image: annotationView.glyphText?.image()) leftIcon.backgroundColor = UIColor(.indigo) annotationView.leftCalloutAccessoryView = leftIcon let subtitle = UILabel() - subtitle.text = "Latitude: \(String(format: "%.5f", positionAnnotation.coordinate.latitude)) \n" + subtitle.text = "Long Name: \(positionAnnotation.nodePosition?.user?.longName ?? "Unknown") \n" + subtitle.text! += "Latitude: \(String(format: "%.5f", positionAnnotation.coordinate.latitude)) \n" subtitle.text! += "Longitude: \(String(format: "%.5f", positionAnnotation.coordinate.longitude)) \n" let distanceFormatter = MKDistanceFormatter() subtitle.text! += "Altitude: \(distanceFormatter.string(fromDistance: Double(positionAnnotation.altitude))) \n" if positionAnnotation.nodePosition?.metadata != nil { + + if DeviceRoles(rawValue: Int(positionAnnotation.nodePosition!.metadata?.role ?? 0)) == DeviceRoles.client || + DeviceRoles(rawValue: Int(positionAnnotation.nodePosition!.metadata?.role ?? 0)) == DeviceRoles.clientMute || + DeviceRoles(rawValue: Int(positionAnnotation.nodePosition!.metadata?.role ?? 0)) == DeviceRoles.routerClient{ + annotationView.glyphImage = UIImage(systemName: "flipphone") + } else if DeviceRoles(rawValue: Int(positionAnnotation.nodePosition!.metadata?.role ?? 0)) == DeviceRoles.repeater { + annotationView.glyphImage = UIImage(systemName: "repeat") + } else if DeviceRoles(rawValue: Int(positionAnnotation.nodePosition!.metadata?.role ?? 0)) == DeviceRoles.router { + annotationView.glyphImage = UIImage(systemName: "wifi.router.fill") + } else if DeviceRoles(rawValue: Int(positionAnnotation.nodePosition!.metadata?.role ?? 0)) == DeviceRoles.tracker { + annotationView.glyphImage = UIImage(systemName: "location.viewfinder") + } + let pf = PositionFlags(rawValue: Int(positionAnnotation.nodePosition?.metadata?.positionFlags ?? 3)) if pf.contains(.Satsinview) { subtitle.text! += "Sats in view: \(String(positionAnnotation.satsInView)) \n" @@ -162,8 +226,12 @@ struct MapViewSwiftUI: UIViewRepresentable { subtitle.text! += "Speed: \(formatter.string(from: Measurement(value: Double(positionAnnotation.speed), unit: UnitSpeed.kilometersPerHour))) \n" } if pf.contains(.Heading) { + + annotationView.glyphImage = UIImage(systemName: "location.north.fill")?.rotate(radians: Float(degreesToRadians(Double(positionAnnotation.heading)))) subtitle.text! += "Heading: \(String(positionAnnotation.heading)) \n" } + } else { + annotationView.glyphImage = UIImage(systemName: "flipphone") } subtitle.text! += positionAnnotation.time?.formatted() ?? "Unknown \n" subtitle.numberOfLines = 0 @@ -173,7 +241,7 @@ struct MapViewSwiftUI: UIViewRepresentable { annotationView.rightCalloutAccessoryView = detailsIcon return annotationView case let waypointAnnotation as WaypointEntity: - let annotationView = mapView.dequeueReusableAnnotationView(withIdentifier: "waypoint") as? MKMarkerAnnotationView ?? MKMarkerAnnotationView(annotation: annotation, reuseIdentifier: "Waypoint") + let annotationView = mapView.dequeueReusableAnnotationView(withIdentifier: "waypoint") as? MKMarkerAnnotationView ?? MKMarkerAnnotationView(annotation: annotation, reuseIdentifier: String(waypointAnnotation.id)) annotationView.tag = Int(waypointAnnotation.id) annotationView.isEnabled = true annotationView.canShowCallout = true @@ -243,9 +311,9 @@ struct MapViewSwiftUI: UIViewRepresentable { } 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) diff --git a/Meshtastic/Views/Nodes/NodeDetail.swift b/Meshtastic/Views/Nodes/NodeDetail.swift index 057cac1b..9e967ed7 100644 --- a/Meshtastic/Views/Nodes/NodeDetail.swift +++ b/Meshtastic/Views/Nodes/NodeDetail.swift @@ -13,6 +13,7 @@ struct NodeDetail: View { @Environment(\.managedObjectContext) var context @EnvironmentObject var bleManager: BLEManager @Environment(\.colorScheme) var colorScheme: ColorScheme + @AppStorage("meshMapType") private var meshMapType = "standard" @State private var mapType: MKMapType = .standard @State var waypointCoordinate: CLLocationCoordinate2D? @State var editingWaypoint: Int = 0 @@ -39,6 +40,7 @@ struct NodeDetail: View { /// The current weather condition for the city. @State private var condition: WeatherCondition? @State private var temperature: Measurement? + @State private var humidity: Int? @State private var symbolName: String = "cloud.fill" @State private var attributionLink: URL? @@ -65,7 +67,9 @@ struct NodeDetail: View { editingWaypoint = wpId presentingWaypointForm = true } - }, positions: annotations, waypoints: Array(waypoints), mapViewType: mapType, + }, positions: annotations, waypoints: Array(waypoints), + mapViewType: mapType, + centeringMode: .allPositions, centerOnPositionsOnly: true, customMapOverlay: self.customMapOverlay, overlays: self.overlays @@ -84,8 +88,12 @@ struct NodeDetail: View { .pickerStyle(.menu) .padding(5) VStack { - Label(temperature?.formatted() ?? "??", systemImage: symbolName) + Label(temperature?.formatted(.measurement(width: .narrow)) ?? "??", systemImage: symbolName) .font(.caption) + + + Label("\(humidity ?? 0)%", systemImage: "humidity") + .font(.caption2) } .padding(10) .background(.thinMaterial, in: RoundedRectangle(cornerRadius: 12, style: .continuous)) @@ -351,12 +359,11 @@ struct NodeDetail: View { } } - if (self.bleManager.connectedPeripheral != nil && self.bleManager.connectedPeripheral.num == node.num) - || (self.bleManager.connectedPeripheral != nil && node.metadata != nil) { + if (self.bleManager.connectedPeripheral != nil && node.metadata != nil) { HStack { let connectedNode = getNodeInfo(id: bleManager.connectedPeripheral.num, context: context) - if node.metadata?.canShutdown ?? false { + if node.metadata?.canShutdown ?? false || hwModelString == "RAK4631" {//node.metadata?.hwModel ?? "UNSET" == "RAK4631" { Button(action: { showingShutdownConfirm = true @@ -436,6 +443,28 @@ struct NodeDetail: View { }) .onAppear { self.bleManager.context = context + switch meshMapType { + case "standard": + mapType = .standard + break + case "mutedStandard": + mapType = .mutedStandard + break + case "hybrid": + mapType = .hybrid + break + case "hybridFlyover": + mapType = .hybridFlyover + break + case "satellite": + mapType = .satellite + break + case "satelliteFlyover": + mapType = .satelliteFlyover + break + default: + mapType = .hybridFlyover + } } .task(id: node.num) { do { @@ -447,6 +476,7 @@ struct NodeDetail: View { let weather = try await WeatherService.shared.weather(for: mostRecent.nodeLocation!) condition = weather.currentWeather.condition temperature = weather.currentWeather.temperature + humidity = Int(weather.currentWeather.humidity * 100) symbolName = weather.currentWeather.symbolName let attribution = try await WeatherService.shared.attribution diff --git a/Meshtastic/Views/Nodes/NodeMap.swift b/Meshtastic/Views/Nodes/NodeMap.swift index b13caa0c..008c5d2b 100644 --- a/Meshtastic/Views/Nodes/NodeMap.swift +++ b/Meshtastic/Views/Nodes/NodeMap.swift @@ -29,6 +29,9 @@ struct NodeMap: View { } } } + @AppStorage("meshMapType") private var meshMapType = "hybridFlyover" + @AppStorage("meshMapCenteringMode") private var meshMapCenteringMode = 0 + //&& nodePosition != nil @FetchRequest(sortDescriptors: [NSSortDescriptor(key: "time", ascending: true)], predicate: NSPredicate(format: "time >= %@ && nodePosition != nil", Calendar.current.startOfDay(for: Date()) as NSDate), animation: .none) @@ -41,6 +44,7 @@ struct NodeMap: View { private var waypoints: FetchedResults @State private var mapType: MKMapType = .standard + @State private var mapCenteringMode: CenteringMode = .allAnnotations @State var waypointCoordinate: CLLocationCoordinate2D = LocationHelper.DefaultLocation @State var editingWaypoint: Int = 0 @State private var presentingWaypointForm = false @@ -71,7 +75,8 @@ struct NodeMap: View { } }, positions: Array(positions), waypoints: Array(waypoints), - mapViewType: mapType ?? MKMapType.standard, + mapViewType: mapType, + centeringMode: mapCenteringMode, centerOnPositionsOnly: false, customMapOverlay: self.customMapOverlay, overlays: self.overlays @@ -109,6 +114,29 @@ struct NodeMap: View { .onAppear(perform: { self.bleManager.context = context self.bleManager.userSettings = userSettings + mapCenteringMode = CenteringMode(rawValue: meshMapCenteringMode) ?? CenteringMode.allAnnotations + switch meshMapType { + case "standard": + mapType = .standard + break + case "mutedStandard": + mapType = .mutedStandard + break + case "hybrid": + mapType = .hybrid + break + case "hybridFlyover": + mapType = .hybridFlyover + break + case "satellite": + mapType = .satellite + break + case "satelliteFlyover": + mapType = .satelliteFlyover + break + default: + mapType = .hybridFlyover + } }) } } diff --git a/Meshtastic/Views/Settings/AppSettings.swift b/Meshtastic/Views/Settings/AppSettings.swift index b0802fb9..133a7b81 100644 --- a/Meshtastic/Views/Settings/AppSettings.swift +++ b/Meshtastic/Views/Settings/AppSettings.swift @@ -13,10 +13,6 @@ struct AppSettings: View { @State private var isPresentingCoreDataResetConfirm = false @State private var preferredDeviceConnected = false - var perferredPeripheral: String { - UserDefaults.standard.object(forKey: "preferredPeripheralName") as? String ?? "" - } - var body: some View { VStack { Form { @@ -39,13 +35,6 @@ struct AppSettings: View { } } .pickerStyle(DefaultPickerStyle()) - - Picker("map.type", selection: $userSettings.meshMapType) { - ForEach(MeshMapType.allCases) { map in - Text(map.description) - } - } - .pickerStyle(DefaultPickerStyle()) } @@ -70,6 +59,31 @@ struct AppSettings: View { .listRowSeparator(.visible) } } + + Section(header: Text("global map options")) { + + Picker("map.type", selection: $userSettings.meshMapType) { + ForEach(MeshMapType.allCases) { map in + Text(map.description) + } + } + .pickerStyle(DefaultPickerStyle()) + } + + Section(header: Text("mesh map options")) { + Picker("map.centering", selection: $userSettings.meshMapCenteringMode) { + ForEach(CenteringMode.allCases) { cm in + Text(cm.description) + } + } + .pickerStyle(DefaultPickerStyle()) + + Toggle(isOn: $userSettings.meshMapRecentering) { + + Label("map.recentering", systemImage: "rectangle.center.inset.filled") + } + .toggleStyle(SwitchToggleStyle(tint: .accentColor)) + } } HStack { Button { diff --git a/Meshtastic/Views/Settings/LicensedUser.swift b/Meshtastic/Views/Settings/LicensedUser.swift deleted file mode 100644 index 113c4c1e..00000000 --- a/Meshtastic/Views/Settings/LicensedUser.swift +++ /dev/null @@ -1,8 +0,0 @@ -// -// LicensedUser.swift -// Meshtastic -// -// Created by Garth Vander Houwen on 2/4/23. -// - -import Foundation diff --git a/Meshtastic/Views/Settings/Settings.swift b/Meshtastic/Views/Settings/Settings.swift index a36e79bb..d22fd37b 100644 --- a/Meshtastic/Views/Settings/Settings.swift +++ b/Meshtastic/Views/Settings/Settings.swift @@ -81,7 +81,7 @@ struct Settings: View { let connectedNode = nodes.first(where: { $0.num == connectedNodeNum }) connectedNodeNum = Int(bleManager.connectedPeripheral != nil ? bleManager.connectedPeripheral.num : 0) - if node?.metadata == nil && node!.num != connectedNodeNum { + if node?.metadata == nil { let adminMessageId = bleManager.requestDeviceMetadata(fromUser: connectedNode!.user!, toUser: node!.user!, adminIndex: connectedNode!.myInfo!.adminIndex, context: context) if adminMessageId > 0 { diff --git a/Meshtastic/Views/Settings/ShareChannels.swift b/Meshtastic/Views/Settings/ShareChannels.swift index da2655ec..79d91a28 100644 --- a/Meshtastic/Views/Settings/ShareChannels.swift +++ b/Meshtastic/Views/Settings/ShareChannels.swift @@ -288,7 +288,7 @@ struct ShareChannels: View { loRaConfig.usePreset = node?.loRaConfig?.usePreset ?? true loRaConfig.channelNum = UInt32(node?.loRaConfig?.channelNum ?? 0) channelSet.loraConfig = loRaConfig - if node != nil && node?.myInfo != nil { + if node?.myInfo?.channels != nil && node?.myInfo?.channels?.count ?? 0 > 0 { for ch in node!.myInfo!.channels!.array as! [ChannelEntity] { if ch.role > 0 { diff --git a/Meshtastic/Views/Settings/UserConfig.swift b/Meshtastic/Views/Settings/UserConfig.swift index dc5a208e..3501397b 100644 --- a/Meshtastic/Views/Settings/UserConfig.swift +++ b/Meshtastic/Views/Settings/UserConfig.swift @@ -48,8 +48,8 @@ struct UserConfig: View { .onChange(of: longName, perform: { value in let totalBytes = longName.utf8.count // Only mess with the value if it is too big - if totalBytes > 36 { - let firstNBytes = Data(longName.utf8.prefix(36)) + if totalBytes > (isLicensed ? 8 : 36) { + let firstNBytes = Data(longName.utf8.prefix(isLicensed ? 8 : 36)) if let maxBytesString = String(data: firstNBytes, encoding: String.Encoding.utf8) { // Set the longName back to the last place where it was the right size longName = maxBytesString @@ -59,7 +59,7 @@ struct UserConfig: View { } .keyboardType(.default) .disableAutocorrection(true) - Text("\(String(isLicensed ? "Call Sign" : "Long Name")) can be up to 36 bytes long.") + Text("\(String(isLicensed ? "Call Sign" : "Long Name")) can be up to \(isLicensed ? "8" : "36") bytes long.") .font(.caption2) HStack { diff --git a/de.lproj/Localizable.strings b/de.lproj/Localizable.strings index 32add33f..f6785b90 100644 --- a/de.lproj/Localizable.strings +++ b/de.lproj/Localizable.strings @@ -137,6 +137,8 @@ "lora"="LoRa"; "lora.config"="LoRa Einstellungen"; "map"="Mesh Karte"; +"map.centering"="Centering"; +"map.recentering"="Automatic Re-centering"; "map.type"="kartentyp"; "mesh.log"="Mesh Log"; "mesh.log.bluetooth.config %@"="Bluetooth Konfiguration empfangen: %@"; diff --git a/en.lproj/Localizable.strings b/en.lproj/Localizable.strings index fc958b77..36951b44 100644 --- a/en.lproj/Localizable.strings +++ b/en.lproj/Localizable.strings @@ -137,7 +137,9 @@ "lora"="LoRa"; "lora.config"="LoRa Config"; "map"="Mesh Map"; -"map.type"="Map Type"; +"map.type"="Default Type"; +"map.centering"="Centering Mode"; +"map.recentering"="Automatic Re-centering"; "mesh.log"="Mesh Log"; "mesh.log.bluetooth.config %@"="Bluetooth config received: %@"; "mesh.log.cannedmessage.config %@"="Canned Message module config received: %@"; diff --git a/zh-Hans.lproj/Localizable.strings b/zh-Hans.lproj/Localizable.strings index bf6c29d1..9a54174b 100644 --- a/zh-Hans.lproj/Localizable.strings +++ b/zh-Hans.lproj/Localizable.strings @@ -137,6 +137,8 @@ "lora"="LoRa"; "lora.config"="LoRa 配置"; "map"="Mesh 地图"; +"map.centering"="Centering"; +"map.recentering"="Automatic Re-centering"; "map.type"="地图类型"; "mesh.log"="Mesh 日志"; "mesh.log.bluetooth.config %@"="Bluetooth config received: %@";