diff --git a/Meshtastic.xcodeproj/project.pbxproj b/Meshtastic.xcodeproj/project.pbxproj index 1353fce8..4e131874 100644 --- a/Meshtastic.xcodeproj/project.pbxproj +++ b/Meshtastic.xcodeproj/project.pbxproj @@ -108,6 +108,9 @@ DDC2E1A726CEB3400042C5E4 /* LocationHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDC2E1A626CEB3400042C5E4 /* LocationHelper.swift */; }; DDC3B274283F411B00AC321C /* LastHeardText.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDC3B273283F411B00AC321C /* LastHeardText.swift */; }; DDC4D568275499A500A4208E /* Persistence.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDC4D567275499A500A4208E /* Persistence.swift */; }; + DDC94FC129CE063B0082EA6E /* BatteryLevel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDC94FC029CE063B0082EA6E /* BatteryLevel.swift */; }; + DDC94FC229CE063B0082EA6E /* BatteryLevel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDC94FC029CE063B0082EA6E /* BatteryLevel.swift */; }; + DDC94FCE29CF55310082EA6E /* RtttlConfig.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDC94FCD29CF55310082EA6E /* RtttlConfig.swift */; }; DDCDC6CB29481FCC004C1DDA /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = DDCDC6CD29481FCC004C1DDA /* Localizable.strings */; }; DDCE4E2C2869F92900BE9F8F /* UserConfig.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDCE4E2B2869F92900BE9F8F /* UserConfig.swift */; }; DDD3BBD5292D763200D609B3 /* MeshtasticTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDD3BBD4292D763200D609B3 /* MeshtasticTests.swift */; }; @@ -282,6 +285,9 @@ DDC2E1A626CEB3400042C5E4 /* LocationHelper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocationHelper.swift; sourceTree = ""; }; DDC3B273283F411B00AC321C /* LastHeardText.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LastHeardText.swift; sourceTree = ""; }; DDC4D567275499A500A4208E /* Persistence.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Persistence.swift; sourceTree = ""; }; + DDC94FC029CE063B0082EA6E /* BatteryLevel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BatteryLevel.swift; sourceTree = ""; }; + DDC94FC329CED7280082EA6E /* MeshtasticDataModelV10.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = MeshtasticDataModelV10.xcdatamodel; sourceTree = ""; }; + DDC94FCD29CF55310082EA6E /* RtttlConfig.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RtttlConfig.swift; sourceTree = ""; }; DDCDC69A29467643004C1DDA /* MeshtasticDataModelV3.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = MeshtasticDataModelV3.xcdatamodel; sourceTree = ""; }; DDCDC6CC29481FCC004C1DDA /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/Localizable.strings; sourceTree = ""; }; DDCDC6CE294821AD004C1DDA /* de */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = de; path = de.lproj/Localizable.strings; sourceTree = ""; }; @@ -455,6 +461,7 @@ DD6193742862F6E600E59241 /* ExternalNotificationConfig.swift */, DD2160AE28C5552500C17253 /* MQTTConfig.swift */, DD41582928585C32009B0E59 /* RangeTestConfig.swift */, + DDC94FCD29CF55310082EA6E /* RtttlConfig.swift */, DD6193782863875F00E59241 /* SerialConfig.swift */, DD415827285859C4009B0E59 /* TelemetryConfig.swift */, ); @@ -677,6 +684,7 @@ DDDE59FC29AF163D00490C6C /* Widgets.swift */, DDDE5A0029AF163E00490C6C /* Info.plist */, DDDE5A0F29AFE69700490C6C /* MeshActivityAttributes.swift */, + DDC94FC029CE063B0082EA6E /* BatteryLevel.swift */, ); path = Widgets; sourceTree = ""; @@ -899,8 +907,10 @@ DDC4D568275499A500A4208E /* Persistence.swift in Sources */, DDD6EEAF29BC024700383354 /* Firmware.swift in Sources */, DD90860E26F69BAE00DC5189 /* NodeMap.swift in Sources */, + DDC94FCE29CF55310082EA6E /* RtttlConfig.swift in Sources */, DD964FBD296E6B01007C176F /* EmojiOnlyTextField.swift in Sources */, DD8169FF272476C700F4AB02 /* LogDocument.swift in Sources */, + DDC94FC129CE063B0082EA6E /* BatteryLevel.swift in Sources */, DD2AD8A8296D2DF9001FF0E7 /* MapViewSwiftUI.swift in Sources */, DD5E5213298EE33B00D21B61 /* deviceonly.pb.swift in Sources */, DD5E5208298EE33B00D21B61 /* rtttl.pb.swift in Sources */, @@ -1003,6 +1013,7 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + DDC94FC229CE063B0082EA6E /* BatteryLevel.swift in Sources */, DDDE5A1129AFE69700490C6C /* MeshActivityAttributes.swift in Sources */, DDDE59FB29AF163D00490C6C /* WidgetsLiveActivity.swift in Sources */, DDDE59FD29AF163D00490C6C /* Widgets.swift in Sources */, @@ -1188,7 +1199,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 2.1.3; + MARKETING_VERSION = 2.1.4; PRODUCT_BUNDLE_IDENTIFIER = gvh.MeshtasticClient; PRODUCT_NAME = "$(TARGET_NAME)"; SUPPORTS_MACCATALYST = YES; @@ -1222,7 +1233,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 2.1.3; + MARKETING_VERSION = 2.1.4; PRODUCT_BUNDLE_IDENTIFIER = gvh.MeshtasticClient; PRODUCT_NAME = "$(TARGET_NAME)"; SUPPORTS_MACCATALYST = YES; @@ -1469,6 +1480,7 @@ DD3CC6BA28E366DF00FA9159 /* Meshtastic.xcdatamodeld */ = { isa = XCVersionGroup; children = ( + DDC94FC329CED7280082EA6E /* MeshtasticDataModelV10.xcdatamodel */, DDDD527729B5B83F0045BC3C /* MeshtasticDataModelV9.xcdatamodel */, DDBA45EC299ED78100DEEDDC /* MeshtasticDataModelV8.xcdatamodel */, DD5E51CC2986643400D21B61 /* MeshtasticDataModelV7.xcdatamodel */, @@ -1479,7 +1491,7 @@ DD5D0A9A2931AD6B00F7EA61 /* MeshtasticDataModelV2.xcdatamodel */, DD3CC6BB28E366DF00FA9159 /* MeshtasticDataModel.xcdatamodel */, ); - currentVersion = DDDD527729B5B83F0045BC3C /* MeshtasticDataModelV9.xcdatamodel */; + currentVersion = DDC94FC329CED7280082EA6E /* MeshtasticDataModelV10.xcdatamodel */; name = Meshtastic.xcdatamodeld; path = Meshtastic/Meshtastic.xcdatamodeld; sourceTree = ""; diff --git a/Meshtastic/Helpers/BLEManager.swift b/Meshtastic/Helpers/BLEManager.swift index c5471674..c37d9757 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) @@ -36,6 +36,7 @@ class BLEManager: NSObject, CBPeripheralDelegate, ObservableObject { var timeoutTimerCount = 0 var timeoutTimerRuns = 0 var positionTimer: Timer? + var lastPosition: CLLocationCoordinate2D? let emptyNodeNum: UInt32 = 4294967295 /* Meshtastic Service Details */ var TORADIO_characteristic: CBCharacteristic! @@ -46,9 +47,9 @@ class BLEManager: NSObject, CBPeripheralDelegate, ObservableObject { 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") - + let meshLog = documentsFolder.appendingPathComponent("meshlog.txt") - + // MARK: init BLEManager override init() { self.lastConnectionError = "" @@ -57,7 +58,7 @@ class BLEManager: NSObject, CBPeripheralDelegate, ObservableObject { 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() { @@ -66,7 +67,7 @@ class BLEManager: NSObject, CBPeripheralDelegate, ObservableObject { print("✅ Scanning Started") } } - + // Stop Scanning For BLE Devices func stopScanning() { if centralManager.isScanning { @@ -74,7 +75,7 @@ class BLEManager: NSObject, CBPeripheralDelegate, ObservableObject { print("🛑 Stopped Scanning") } } - + // MARK: BLE Connect functions /// The action after the timeout-timer has fired /// @@ -84,25 +85,25 @@ class BLEManager: NSObject, CBPeripheralDelegate, ObservableObject { @objc func timeoutTimerFired(timer: Timer) { guard let timerContext = timer.userInfo as? [String: String] else { return } let name: String = timerContext["name", default: "Unknown"] - + self.timeoutTimerCount += 1 self.lastConnectionError = "" - + if timeoutTimerCount == 10 { if connectedPeripheral != nil { self.centralManager?.cancelPeripheralConnection(connectedPeripheral.peripheral) } connectedPeripheral = nil if self.timeoutTimer != nil { - + self.timeoutTimer!.invalidate() } self.isConnected = false self.isConnecting = false self.lastConnectionError = "🚨 " + String.localizedStringWithFormat(NSLocalizedString("ble.connection.timeout %d %@", - comment: "Connection failed after %d attempts to connect to %@. You may need to forget your device under Settings > Bluetooth."), - timeoutTimerCount, name) - + comment: "Connection failed after %d attempts to connect to %@. You may need to forget your device under Settings > Bluetooth."), + timeoutTimerCount, name) + MeshLogger.log(lastConnectionError) self.timeoutTimerCount = 0 self.timeoutTimerRuns += 1 @@ -111,7 +112,7 @@ class BLEManager: NSObject, CBPeripheralDelegate, ObservableObject { print("🚨 BLE Connecting 2 Second Timeout Timer Fired \(timeoutTimerCount) Time(s): \(name)") } } - + // Connect to a specific peripheral func connectTo(peripheral: CBPeripheral) { stopScanning() @@ -124,7 +125,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 { @@ -137,10 +138,10 @@ class BLEManager: NSObject, CBPeripheralDelegate, ObservableObject { RunLoop.current.add(timeoutTimer!, forMode: .common) print("ℹ️ BLE Connecting: \(peripheral.name ?? "Unknown")") } - + // Disconnect Connected Peripheral func disconnectPeripheral(reconnect: Bool = true) { - + guard let connectedPeripheral = connectedPeripheral else { return } automaticallyReconnect = reconnect centralManager?.cancelPeripheralConnection(connectedPeripheral.peripheral) @@ -151,7 +152,7 @@ class BLEManager: NSObject, CBPeripheralDelegate, ObservableObject { connectedVersion = "0.0.0" startScanning() } - + // Called each time a peripheral is discovered func centralManager(_ central: CBCentralManager, didConnect peripheral: CBPeripheral) { isConnecting = false @@ -165,7 +166,7 @@ class BLEManager: NSObject, CBPeripheralDelegate, ObservableObject { if timeoutTimer != nil { timeoutTimer!.invalidate() } - + // remove any connection errors self.lastConnectionError = "" // Map the peripheral to the connectedPeripheral ObservedObjects @@ -182,13 +183,13 @@ class BLEManager: NSObject, CBPeripheralDelegate, ObservableObject { peripheral.discoverServices([meshtasticServiceCBUUID]) print("✅ BLE Connected: \(peripheral.name ?? "Unknown")") } - + // Called when a Peripheral fails to connect func centralManager(_ central: CBCentralManager, didFailToConnect peripheral: CBPeripheral, error: Error?) { disconnectPeripheral() print("🚫 BLE Failed to Connect: \(peripheral.name ?? "Unknown")") } - + // Disconnect Peripheral Event func centralManager(_ central: CBCentralManager, didDisconnectPeripheral peripheral: CBPeripheral, error: Error?) { self.connectedPeripheral = nil @@ -201,8 +202,8 @@ class BLEManager: NSObject, CBPeripheralDelegate, ObservableObject { if errorCode == 6 { // CBError.Code.connectionTimeout The connection has timed out unexpectedly. // Happens when device is manually reset / powered off lastConnectionError = "🚨" + String.localizedStringWithFormat(NSLocalizedString("ble.errorcode.6 %@", - comment: "The app will automatically reconnect to the preferred radio if it come back in range."), - e.localizedDescription) + comment: "The app will automatically reconnect to the preferred radio if it come back in range."), + e.localizedDescription) print("🚨 BLE Disconnected: \(peripheral.name ?? "Unknown") Error Code: \(errorCode) Error: \(e.localizedDescription)") } else if errorCode == 7 { // CBError.Code.peripheralDisconnected The specified device has disconnected from us. // Seems to be what is received when a tbeam sleeps, immediately recconnecting does not work. @@ -211,8 +212,8 @@ class BLEManager: NSObject, CBPeripheralDelegate, ObservableObject { } else if errorCode == 14 { // Peer removed pairing information // Forgetting and reconnecting seems to be necessary so we need to show the user an error telling them to do that lastConnectionError = "🚨 " + String.localizedStringWithFormat(NSLocalizedString("ble.errorcode.14 %@", - comment: "This error usually cannot be fixed without forgetting the device unders Settings > Bluetooth and re-connecting to the radio."), - e.localizedDescription) + comment: "This error usually cannot be fixed without forgetting the device unders Settings > Bluetooth and re-connecting to the radio."), + e.localizedDescription) print("🚨 BLE Disconnected: \(peripheral.name ?? "Unknown") Error Code: \(errorCode) Error: \(lastConnectionError)") } else { lastConnectionError = "🚨 \(e.localizedDescription)" @@ -226,7 +227,7 @@ class BLEManager: NSObject, CBPeripheralDelegate, ObservableObject { // Start a scan so the disconnected peripheral is moved to the peripherals[] if it is awake self.startScanning() } - + // MARK: Peripheral Services functions func peripheral(_ peripheral: CBPeripheral, didDiscoverServices error: Error?) { if let e = error { @@ -240,36 +241,36 @@ class BLEManager: NSObject, CBPeripheralDelegate, ObservableObject { } } } - + // MARK: Discover Characteristics Event func peripheral(_ peripheral: CBPeripheral, didDiscoverCharacteristicsFor service: CBService, error: Error?) { - + if let e = error { print("🚫 BLE Discover Characteristics error for \(peripheral.name ?? "Unknown") \(e) disconnecting device") // Try and stop crashes when this error occurs disconnectPeripheral() return } - + guard let characteristics = service.characteristics else { return } - + for characteristic in characteristics { switch characteristic.uuid { - + case TORADIO_UUID: print("✅ BLE did discover TORADIO characteristic for Meshtastic by \(peripheral.name ?? "Unknown")") TORADIO_characteristic = characteristic - + case FROMRADIO_UUID: print("✅ BLE did discover FROMRADIO characteristic for Meshtastic by \(peripheral.name ?? "Unknown")") FROMRADIO_characteristic = characteristic peripheral.readValue(for: FROMRADIO_characteristic) - + case FROMNUM_UUID: print("✅ BLE did discover FROMNUM (Notify) characteristic for Meshtastic by \(peripheral.name ?? "Unknown")") FROMNUM_characteristic = characteristic peripheral.setNotifyValue(true, for: characteristic) - + default: break } @@ -278,11 +279,11 @@ class BLEManager: NSObject, CBPeripheralDelegate, ObservableObject { sendWantConfig() } } - + func requestDeviceMetadata(fromUser: UserEntity, toUser: UserEntity, adminIndex: Int32, context: NSManagedObjectContext) -> Int64 { - + guard connectedPeripheral!.peripheral.state == CBPeripheralState.connected else { return 0 } - + var adminPacket = AdminMessage() adminPacket.getDeviceMetadataRequest = true var meshPacket: MeshPacket = MeshPacket() @@ -303,12 +304,12 @@ class BLEManager: NSObject, CBPeripheralDelegate, ObservableObject { } return 0 } - + func sendTraceRouteRequest(destNum: Int64, wantResponse: Bool) -> Bool { - + var success = false guard connectedPeripheral!.peripheral.state == CBPeripheralState.connected else { return success } - + let fromNodeNum = connectedPeripheral.num let routePacket = RouteDiscovery() var meshPacket = MeshPacket() @@ -320,100 +321,100 @@ class BLEManager: NSObject, CBPeripheralDelegate, ObservableObject { dataMessage.portnum = PortNum.tracerouteApp dataMessage.wantResponse = wantResponse meshPacket.decoded = dataMessage - + var toRadio: ToRadio! toRadio = ToRadio() toRadio.packet = meshPacket let binaryData: Data = try! toRadio.serializedData() - + if connectedPeripheral!.peripheral.state == CBPeripheralState.connected { connectedPeripheral.peripheral.writeValue(binaryData, for: TORADIO_characteristic, type: .withResponse) success = true - + let logString = String.localizedStringWithFormat(NSLocalizedString("mesh.log.traceroute.sent %@", - comment: "Sent a Trace Route Request to node: %@"), String(destNum)) + comment: "Sent a Trace Route Request to node: %@"), String(destNum)) MeshLogger.log("🪧 \(logString)") } return success } - + func sendWantConfig() { guard connectedPeripheral!.peripheral.state == CBPeripheralState.connected else { return } - + if FROMRADIO_characteristic == nil { MeshLogger.log("🚨 \(NSLocalizedString("firmware.version.unsupported", comment: "Unsupported Firmware Version Detected, unable to connect to device."))") invalidVersion = true return } else { - - let nodeName = connectedPeripheral!.peripheral.name ?? NSLocalizedString("unknown", comment: NSLocalizedString("unknown", comment: "Unknown")) - let logString = String.localizedStringWithFormat(NSLocalizedString("mesh.log.wantconfig %@", comment: "Issuing Want Config to %@"), nodeName) - MeshLogger.log("🛎️ \(logString)") - // BLE Characteristics discovered, issue wantConfig - var toRadio: ToRadio = ToRadio() - configNonce += 1 - toRadio.wantConfigID = configNonce - let binaryData: Data = try! toRadio.serializedData() - connectedPeripheral!.peripheral.writeValue(binaryData, for: TORADIO_characteristic, type: .withResponse) + + let nodeName = connectedPeripheral!.peripheral.name ?? NSLocalizedString("unknown", comment: NSLocalizedString("unknown", comment: "Unknown")) + let logString = String.localizedStringWithFormat(NSLocalizedString("mesh.log.wantconfig %@", comment: "Issuing Want Config to %@"), nodeName) + MeshLogger.log("🛎️ \(logString)") + // BLE Characteristics discovered, issue wantConfig + var toRadio: ToRadio = ToRadio() + configNonce += 1 + toRadio.wantConfigID = configNonce + 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) } } - + func peripheral(_ peripheral: CBPeripheral, didUpdateNotificationStateFor characteristic: CBCharacteristic, error: Error?) { if let errorText = error?.localizedDescription { print("🚫 didUpdateNotificationStateFor error: \(errorText)") } } - + // MARK: Data Read / Update Characteristic Event func peripheral(_ peripheral: CBPeripheral, didUpdateValueFor characteristic: CBCharacteristic, error: Error?) { - + if let e = error { - + print("🚫 didUpdateValueFor Characteristic error \(e)") - + let errorCode = (e as NSError).code - + if errorCode == 5 || errorCode == 15 { // BLE PIN connection errors // 5 CBATTErrorDomain Code=5 "Authentication is insufficient." // 15 CBATTErrorDomain Code=15 "Encryption is insufficient." lastConnectionError = "🚨" + String.localizedStringWithFormat(NSLocalizedString("ble.errorcode.pin %@", - comment: "Please try connecting again and check the PIN carefully."), - e.localizedDescription) + comment: "Please try connecting again and check the PIN carefully."), + e.localizedDescription) print("🚨 \(e.localizedDescription) Please try connecting again and check the PIN carefully.") self.disconnectPeripheral(reconnect: false) } } - + switch characteristic.uuid { - + case FROMRADIO_UUID: - + if characteristic.value == nil || characteristic.value!.isEmpty { return } var decodedInfo = FromRadio() - + do { decodedInfo = try FromRadio(serializedData: characteristic.value!) - + } catch { print(characteristic.value!) } - + switch decodedInfo.packet.decoded.portnum { - - // Handle Any local only packets we get over BLE + + // Handle Any local only packets we get over BLE case .unknownApp: var nowKnown = false - + // MyInfo from initial connection if decodedInfo.myInfo.isInitialized && decodedInfo.myInfo.myNodeNum > 0 { - + let lastDotIndex = decodedInfo.myInfo.firmwareVersion.lastIndex(of: ".") - + if lastDotIndex == nil { invalidVersion = true connectedVersion = "0.0.0" @@ -422,17 +423,17 @@ class BLEManager: NSObject, CBPeripheralDelegate, ObservableObject { nowKnown = true connectedVersion = String(version.dropLast()) } - + let supportedVersion = connectedVersion == "0.0.0" || self.minimumVersion.compare(connectedVersion, options: .numeric) == .orderedAscending || minimumVersion.compare(connectedVersion, options: .numeric) == .orderedSame if !supportedVersion { invalidVersion = true lastConnectionError = "🚨" + NSLocalizedString("update.firmware", comment: "Update Your Firmware") return - + } else { - + let myInfo = myInfoPacket(myInfo: decodedInfo.myInfo, peripheralId: self.connectedPeripheral.id, context: context!) - + if myInfo != nil { connectedPeripheral.num = myInfo!.myNodeNum connectedPeripheral.firmwareVersion = myInfo?.firmwareVersion ?? NSLocalizedString("unknown", comment: "Unknown") @@ -446,7 +447,7 @@ class BLEManager: NSObject, CBPeripheralDelegate, ObservableObject { if decodedInfo.nodeInfo.num > 0 && !invalidVersion { nowKnown = true let nodeInfo = nodeInfoPacket(nodeInfo: decodedInfo.nodeInfo, channel: decodedInfo.packet.channel, context: context!) - + if nodeInfo != nil { if self.connectedPeripheral != nil && self.connectedPeripheral.num == nodeInfo!.num { if nodeInfo!.user != nil { @@ -463,18 +464,18 @@ class BLEManager: NSObject, CBPeripheralDelegate, ObservableObject { } // Config if decodedInfo.config.isInitialized && !invalidVersion { - + nowKnown = true localConfig(config: decodedInfo.config, context: context!, nodeNum: self.connectedPeripheral.num, nodeLongName: self.connectedPeripheral.longName) } // Module Config if decodedInfo.moduleConfig.isInitialized && !invalidVersion { - + nowKnown = true moduleConfig(config: decodedInfo.moduleConfig, context: context!, nodeNum: self.connectedPeripheral.num, nodeLongName: self.connectedPeripheral.longName) - + if decodedInfo.moduleConfig.payloadVariant == ModuleConfig.OneOf_PayloadVariant.cannedMessage(decodedInfo.moduleConfig.cannedMessage) { - + if decodedInfo.moduleConfig.cannedMessage.enabled { _ = self.getCannedMessageModuleMessages(destNum: self.connectedPeripheral.num, wantResponse: true) } @@ -487,7 +488,7 @@ class BLEManager: NSObject, CBPeripheralDelegate, ObservableObject { } // Log any other unknownApp calls if !nowKnown { MeshLogger.log("🕸️ MESH PACKET received for Unknown App UNHANDLED \((try? decodedInfo.packet.jsonString()) ?? "JSON Decode Failure")") } - + case .textMessageApp: textMessageAppPacket(packet: decodedInfo.packet, connectedNode: (self.connectedPeripheral != nil ? connectedPeripheral.num : 0), context: context!) case .remoteHardwareApp: @@ -513,7 +514,7 @@ class BLEManager: NSObject, CBPeripheralDelegate, ObservableObject { case .rangeTestApp: MeshLogger.log("🕸️ MESH PACKET received for Range Test App UNHANDLED \((try? decodedInfo.packet.jsonString()) ?? "JSON Decode Failure")") case .telemetryApp: - if !invalidVersion { telemetryPacket(packet: decodedInfo.packet, connectedNode: (self.connectedPeripheral != nil ? connectedPeripheral.num : 0), context: context!) } + if !invalidVersion { telemetryPacket(packet: decodedInfo.packet, connectedNode: (self.connectedPeripheral != nil ? connectedPeripheral.num : 0), context: context!) } case .textMessageCompressedApp: MeshLogger.log("🕸️ MESH PACKET received for Text Message Compressed App UNHANDLED \((try? decodedInfo.packet.jsonString()) ?? "JSON Decode Failure")") case .zpsApp: @@ -527,34 +528,34 @@ class BLEManager: NSObject, CBPeripheralDelegate, ObservableObject { case .audioApp: MeshLogger.log("🕸️ MESH PACKET received for Audio App UNHANDLED \((try? decodedInfo.packet.jsonString()) ?? "JSON Decode Failure")") case .tracerouteApp: - if let routingMessage = try? RouteDiscovery(serializedData: decodedInfo.packet.decoded.payload) { - - if routingMessage.route.count == 0 { - let logString = String.localizedStringWithFormat(NSLocalizedString("mesh.log.traceroute.received.direct %@", - comment: "Trace Route request sent to node: %@ was recieived directly."), String(decodedInfo.packet.from)) - MeshLogger.log("🪧 \(logString)") - } else { - - var routeString = "\(decodedInfo.packet.to) --> " - for node in routingMessage.route { - routeString += "\(node) --> " - } - routeString += "\(decodedInfo.packet.from)" - let logString = String.localizedStringWithFormat(NSLocalizedString("mesh.log.traceroute.received.route %@", - comment: "Trace Route request returned: %@"), routeString) - MeshLogger.log("🪧 \(logString)") + if let routingMessage = try? RouteDiscovery(serializedData: decodedInfo.packet.decoded.payload) { + + if routingMessage.route.count == 0 { + let logString = String.localizedStringWithFormat(NSLocalizedString("mesh.log.traceroute.received.direct %@", + comment: "Trace Route request sent to node: %@ was recieived directly."), String(decodedInfo.packet.from)) + MeshLogger.log("🪧 \(logString)") + } else { + + var routeString = "\(decodedInfo.packet.to) --> " + for node in routingMessage.route { + routeString += "\(node) --> " } + routeString += "\(decodedInfo.packet.from)" + let logString = String.localizedStringWithFormat(NSLocalizedString("mesh.log.traceroute.received.route %@", + comment: "Trace Route request returned: %@"), routeString) + MeshLogger.log("🪧 \(logString)") } + } case .UNRECOGNIZED: MeshLogger.log("🕸️ MESH PACKET received for Other App UNHANDLED \(try! decodedInfo.packet.jsonString())") case .max: print("MAX PORT NUM OF 511") } - + // MARK: Check for an All / Broadcast User and delete it as a transition to multi channel let fetchBCUserRequest: NSFetchRequest = NSFetchRequest.init(entityName: "UserEntity") fetchBCUserRequest.predicate = NSPredicate(format: "num == %lld", Int64(emptyNodeNum)) - + do { guard let fetchedUser = try context?.fetch(fetchBCUserRequest) as? [UserEntity] else { return @@ -566,7 +567,7 @@ class BLEManager: NSObject, CBPeripheralDelegate, ObservableObject { } catch { print("💥 Error Deleting the All - Broadcast User") } - + if decodedInfo.configCompleteID != 0 && decodedInfo.configCompleteID == configNonce { invalidVersion = false lastConnectionError = "" @@ -589,7 +590,7 @@ class BLEManager: NSObject, CBPeripheralDelegate, ObservableObject { } return } - + case FROMNUM_UUID: print("🗞️ BLE (Notify) characteristic, value will be read next") default: @@ -600,16 +601,16 @@ class BLEManager: NSObject, CBPeripheralDelegate, ObservableObject { peripheral.readValue(for: FROMRADIO_characteristic) } } - + public func sendMessage(message: String, toUserNum: Int64, channel: Int32, isEmoji: Bool, replyID: Int64) -> Bool { var success = false - + // Return false if we are not properly connected to a device, handle retry logic in the view for now if connectedPeripheral == nil || connectedPeripheral!.peripheral.state != CBPeripheralState.connected { - + self.disconnectPeripheral() self.startScanning() - + // Try and connect to the preferredPeripherial first let preferredPeripheral = peripherals.filter({ $0.peripheral.identifier.uuidString == UserDefaults.standard.object(forKey: "preferredPeripheralId") as? String ?? "" }).first if preferredPeripheral != nil && preferredPeripheral?.peripheral != nil { @@ -617,34 +618,34 @@ class BLEManager: NSObject, CBPeripheralDelegate, ObservableObject { } 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) + comment: "Message Send Failed, not properly connected to %@"), nodeName) MeshLogger.log("🚫 \(logString)") - + success = false } else if message.count < 1 { - + // Don't send an empty message print("🚫 Don't Send an Empty Message") success = false - + } else { - + let fromUserNum: Int64 = self.connectedPeripheral.num - + let messageUsers: NSFetchRequest = NSFetchRequest.init(entityName: "UserEntity") messageUsers.predicate = NSPredicate(format: "num IN %@", [fromUserNum, Int64(toUserNum)]) - + do { - + guard let fetchedUsers = try context?.fetch(messageUsers) as? [UserEntity] else { return false } if fetchedUsers.isEmpty { - + print("🚫 Message Users Not Found, Fail") success = false } else if fetchedUsers.count >= 1 { - + let newMessage = MessageEntity(context: context!) newMessage.messageId = Int64(UInt32.random(in: UInt32(UInt8.max).. 0 { @@ -686,7 +687,7 @@ class BLEManager: NSObject, CBPeripheralDelegate, ObservableObject { meshPacket.decoded.replyID = UInt32(replyID) } meshPacket.wantAck = true - + var toRadio: ToRadio! toRadio = ToRadio() toRadio.packet = meshPacket @@ -699,7 +700,7 @@ class BLEManager: NSObject, CBPeripheralDelegate, ObservableObject { try context!.save() print("💾 Saved a new sent message from \(connectedPeripheral.num) to \(toUserNum)") success = true - + } catch { context!.rollback() let nsError = error as NSError @@ -707,14 +708,14 @@ class BLEManager: NSObject, CBPeripheralDelegate, ObservableObject { } } } - + } catch { - + } } return success } - + public func sendWaypoint(waypoint: Waypoint) -> Bool { if waypoint.latitudeI == 373346000 && waypoint.longitudeI == -1220090000 { return false @@ -771,14 +772,25 @@ class BLEManager: NSObject, CBPeripheralDelegate, ObservableObject { } return success } - - public func sendPosition(destNum: Int64, wantResponse: Bool) -> Bool { + + public func sendPosition(destNum: Int64, wantResponse: Bool, smartPosition: Bool) -> Bool { var success = false let fromNodeNum = connectedPeripheral.num if fromNodeNum <= 0 || LocationHelper.currentLocation.distance(from: LocationHelper.DefaultLocation) == 0.0 { return false } - + + if smartPosition { + if lastPosition != nil { + let connectedNode = getNodeInfo(id: connectedPeripheral?.num ?? 0, context: context!) + if connectedNode?.positionConfig?.smartPositionEnabled ?? false { + if lastPosition!.distance(from: LocationHelper.currentLocation) < Double(connectedNode?.positionConfig?.broadcastSmartMinimumDistance ?? 50) { + return false + } + } + } + } + lastPosition = LocationHelper.currentLocation var positionPacket = Position() positionPacket.latitudeI = Int32(LocationHelper.currentLocation.latitude * 1e7) positionPacket.longitudeI = Int32(LocationHelper.currentLocation.longitude * 1e7) @@ -800,7 +812,7 @@ class BLEManager: NSObject, CBPeripheralDelegate, ObservableObject { dataMessage.portnum = PortNum.positionApp dataMessage.wantResponse = wantResponse meshPacket.decoded = dataMessage - + var toRadio: ToRadio! toRadio = ToRadio() toRadio.packet = meshPacket @@ -818,14 +830,11 @@ class BLEManager: NSObject, CBPeripheralDelegate, ObservableObject { 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 position to device") - } + let _ = sendPosition(destNum: connectedPeripheral.num, wantResponse: false, smartPosition: true) } } } - + public func sendShutdown(fromUser: UserEntity, toUser: UserEntity, adminIndex: Int32) -> Bool { var adminPacket = AdminMessage() adminPacket.shutdownSeconds = 5 @@ -846,7 +855,7 @@ class BLEManager: NSObject, CBPeripheralDelegate, ObservableObject { } return false } - + public func sendReboot(fromUser: UserEntity, toUser: UserEntity, adminIndex: Int32) -> Bool { var adminPacket = AdminMessage() adminPacket.rebootSeconds = 5 @@ -867,7 +876,7 @@ class BLEManager: NSObject, CBPeripheralDelegate, ObservableObject { } return false } - + public func sendRebootOta(fromUser: UserEntity, toUser: UserEntity, adminIndex: Int32) -> Bool { var adminPacket = AdminMessage() adminPacket.rebootOtaSeconds = 5 @@ -888,7 +897,7 @@ class BLEManager: NSObject, CBPeripheralDelegate, ObservableObject { } return false } - + public func sendFactoryReset(fromUser: UserEntity, toUser: UserEntity) -> Bool { var adminPacket = AdminMessage() adminPacket.factoryReset = 5 @@ -902,14 +911,14 @@ class BLEManager: NSObject, CBPeripheralDelegate, ObservableObject { dataMessage.payload = try! adminPacket.serializedData() dataMessage.portnum = PortNum.adminApp meshPacket.decoded = dataMessage - + let messageDescription = "🚀 Sent Factory Reset Admin Message to: \(toUser.longName ?? NSLocalizedString("unknown", comment: "")) from: \(fromUser.longName ?? NSLocalizedString("unknown", comment: ""))" if sendAdminMessageToRadio(meshPacket: meshPacket, adminDescription: messageDescription, fromUser: fromUser, toUser: toUser) { return true } return false } - + public func sendNodeDBReset(fromUser: UserEntity, toUser: UserEntity) -> Bool { var adminPacket = AdminMessage() adminPacket.nodedbReset = 5 @@ -922,7 +931,7 @@ class BLEManager: NSObject, CBPeripheralDelegate, ObservableObject { var dataMessage = DataMessage() dataMessage.payload = try! adminPacket.serializedData() dataMessage.portnum = PortNum.adminApp - + meshPacket.decoded = dataMessage let messageDescription = "🚀 Sent NodeDB Reset Admin Message to: \(toUser.longName ?? NSLocalizedString("unknown", comment: "")) from: \(fromUser.longName ?? NSLocalizedString("unknown", comment: ""))" if sendAdminMessageToRadio(meshPacket: meshPacket, adminDescription: messageDescription, fromUser: fromUser, toUser: toUser) { @@ -930,7 +939,7 @@ class BLEManager: NSObject, CBPeripheralDelegate, ObservableObject { } return false } - + public func connectToPreferredPeripheral() -> Bool { var success = false // Return false if we are not properly connected to a device, handle retry logic in the view for now @@ -948,9 +957,9 @@ class BLEManager: NSObject, CBPeripheralDelegate, ObservableObject { } return success } - + public func getChannel(channel: Channel, fromUser: UserEntity, toUser: UserEntity) -> Int64 { - + var adminPacket = AdminMessage() adminPacket.getChannelRequest = UInt32(channel.index + 1) var meshPacket: MeshPacket = MeshPacket() @@ -963,7 +972,7 @@ class BLEManager: NSObject, CBPeripheralDelegate, ObservableObject { dataMessage.portnum = PortNum.adminApp dataMessage.wantResponse = true meshPacket.decoded = dataMessage - + let messageDescription = "🎛️ Requested Channel \(channel.index) for \(toUser.longName ?? NSLocalizedString("unknown", comment: "Unknown"))" if sendAdminMessageToRadio(meshPacket: meshPacket, adminDescription: messageDescription, fromUser: fromUser, toUser: toUser) { return Int64(meshPacket.id) @@ -971,7 +980,7 @@ class BLEManager: NSObject, CBPeripheralDelegate, ObservableObject { return 0 } public func saveChannel(channel: Channel, fromUser: UserEntity, toUser: UserEntity) -> Int64 { - + var adminPacket = AdminMessage() adminPacket.setChannel = channel var meshPacket: MeshPacket = MeshPacket() @@ -984,20 +993,20 @@ class BLEManager: NSObject, CBPeripheralDelegate, ObservableObject { dataMessage.portnum = PortNum.adminApp dataMessage.wantResponse = true meshPacket.decoded = dataMessage - + let messageDescription = "🛟 Saved Channel \(channel.index) for \(toUser.longName ?? NSLocalizedString("unknown", comment: "Unknown"))" if sendAdminMessageToRadio(meshPacket: meshPacket, adminDescription: messageDescription, fromUser: fromUser, toUser: toUser) { return Int64(meshPacket.id) } return 0 } - + public func saveChannelSet(base64UrlString: String) -> Bool { if isConnected { // Before we get started delete the existing channels from the myNodeInfo let fetchMyInfoRequest: NSFetchRequest = NSFetchRequest.init(entityName: "MyInfoEntity") fetchMyInfoRequest.predicate = NSPredicate(format: "myNodeNum == %lld", Int64(connectedPeripheral.num)) - + tryClearExistingChannels() let decodedString = base64UrlString.base64urlToBase64() if let decodedData = Data(base64Encoded: decodedString) { @@ -1057,8 +1066,8 @@ class BLEManager: NSObject, CBPeripheralDelegate, ObservableObject { let binaryData: Data = try! toRadio.serializedData() if connectedPeripheral!.peripheral.state == CBPeripheralState.connected { self.connectedPeripheral.peripheral.writeValue(binaryData, for: self.TORADIO_characteristic, type: .withResponse) - let logString = String.localizedStringWithFormat(NSLocalizedString("mesh.log.lora.config.sent %@", comment: "Sent a LoRaConfig for: %@"), String(connectedPeripheral.num)) - MeshLogger.log("📻 \(logString)") + let logString = String.localizedStringWithFormat(NSLocalizedString("mesh.log.lora.config.sent %@", comment: "Sent a LoRaConfig for: %@"), String(connectedPeripheral.num)) + MeshLogger.log("📻 \(logString)") } return true } catch { @@ -1068,7 +1077,7 @@ class BLEManager: NSObject, CBPeripheralDelegate, ObservableObject { } return false } - + public func saveUser(config: User, fromUser: UserEntity, toUser: UserEntity, adminIndex: Int32) -> Int64 { var adminPacket = AdminMessage() adminPacket.setOwner = config @@ -1089,7 +1098,7 @@ class BLEManager: NSObject, CBPeripheralDelegate, ObservableObject { } return 0 } - + public func saveLicensedUser(ham: HamParameters, fromUser: UserEntity, toUser: UserEntity, adminIndex: Int32) -> Int64 { var adminPacket = AdminMessage() adminPacket.setHamMode = ham @@ -1125,20 +1134,20 @@ class BLEManager: NSObject, CBPeripheralDelegate, ObservableObject { dataMessage.portnum = PortNum.adminApp meshPacket.decoded = dataMessage let messageDescription = "🛟 Saved Bluetooth Config for \(toUser.longName ?? NSLocalizedString("unknown", comment: "Unknown"))" - + if sendAdminMessageToRadio(meshPacket: meshPacket, adminDescription: messageDescription, fromUser: fromUser, toUser: toUser) { upsertBluetoothConfigPacket(config: config, nodeNum: toUser.num, context: context!) return Int64(meshPacket.id) } - + return 0 } - + public func saveDeviceConfig(config: Config.DeviceConfig, fromUser: UserEntity, toUser: UserEntity, adminIndex: Int32) -> Int64 { - + var adminPacket = AdminMessage() adminPacket.setConfig.device = config - + var meshPacket: MeshPacket = MeshPacket() meshPacket.to = UInt32(toUser.num) meshPacket.from = UInt32(fromUser.num) @@ -1157,7 +1166,7 @@ class BLEManager: NSObject, CBPeripheralDelegate, ObservableObject { } return 0 } - + public func saveDisplayConfig(config: Config.DisplayConfig, fromUser: UserEntity, toUser: UserEntity, adminIndex: Int32) -> Int64 { var adminPacket = AdminMessage() adminPacket.setConfig.display = config @@ -1181,9 +1190,9 @@ class BLEManager: NSObject, CBPeripheralDelegate, ObservableObject { } return 0 } - + public func saveLoRaConfig(config: Config.LoRaConfig, fromUser: UserEntity, toUser: UserEntity, adminIndex: Int32) -> Int64 { - + var adminPacket = AdminMessage() adminPacket.setConfig.lora = config var meshPacket: MeshPacket = MeshPacket() @@ -1198,20 +1207,20 @@ class BLEManager: NSObject, CBPeripheralDelegate, ObservableObject { dataMessage.portnum = PortNum.adminApp meshPacket.decoded = dataMessage let messageDescription = "🛟 Saved LoRa Config for \(toUser.longName ?? NSLocalizedString("unknown", comment: "Unknown"))" - + if sendAdminMessageToRadio(meshPacket: meshPacket, adminDescription: messageDescription, fromUser: fromUser, toUser: toUser) { upsertLoRaConfigPacket(config: config, nodeNum: toUser.num, context: context!) return Int64(meshPacket.id) } - + return 0 } - + public func savePositionConfig(config: Config.PositionConfig, fromUser: UserEntity, toUser: UserEntity, adminIndex: Int32) -> Int64 { - + var adminPacket = AdminMessage() adminPacket.setConfig.position = config - + var meshPacket: MeshPacket = MeshPacket() meshPacket.to = UInt32(toUser.num) meshPacket.from = UInt32(fromUser.num) @@ -1222,24 +1231,24 @@ class BLEManager: NSObject, CBPeripheralDelegate, ObservableObject { var dataMessage = DataMessage() dataMessage.payload = try! adminPacket.serializedData() dataMessage.portnum = PortNum.adminApp - + meshPacket.decoded = dataMessage - + let messageDescription = "🛟 Saved Position Config for \(toUser.longName ?? NSLocalizedString("unknown", comment: "Unknown"))" - + if sendAdminMessageToRadio(meshPacket: meshPacket, adminDescription: messageDescription, fromUser: fromUser, toUser: toUser) { upsertPositionConfigPacket(config: config, nodeNum: toUser.num, context: context!) return Int64(meshPacket.id) } - + return 0 } - + public func saveNetworkConfig(config: Config.NetworkConfig, fromUser: UserEntity, toUser: UserEntity, adminIndex: Int32) -> Int64 { - + var adminPacket = AdminMessage() adminPacket.setConfig.network = config - + var meshPacket: MeshPacket = MeshPacket() meshPacket.to = UInt32(toUser.num) meshPacket.from = UInt32(fromUser.num) @@ -1250,24 +1259,24 @@ class BLEManager: NSObject, CBPeripheralDelegate, ObservableObject { var dataMessage = DataMessage() dataMessage.payload = try! adminPacket.serializedData() dataMessage.portnum = PortNum.adminApp - + meshPacket.decoded = dataMessage - + let messageDescription = "🛟 Saved Network Config for \(toUser.longName ?? NSLocalizedString("unknown", comment: "Unknown"))" - + if sendAdminMessageToRadio(meshPacket: meshPacket, adminDescription: messageDescription, fromUser: fromUser, toUser: toUser) { upsertNetworkConfigPacket(config: config, nodeNum: toUser.num, context: context!) return Int64(meshPacket.id) } - + return 0 } - + public func saveCannedMessageModuleConfig(config: ModuleConfig.CannedMessageConfig, fromUser: UserEntity, toUser: UserEntity, adminIndex: Int32) -> Int64 { - + var adminPacket = AdminMessage() adminPacket.setModuleConfig.cannedMessage = config - + var meshPacket: MeshPacket = MeshPacket() meshPacket.to = UInt32(toUser.num) meshPacket.from = UInt32(fromUser.num) @@ -1279,22 +1288,22 @@ class BLEManager: NSObject, CBPeripheralDelegate, ObservableObject { dataMessage.payload = try! adminPacket.serializedData() dataMessage.portnum = PortNum.adminApp meshPacket.decoded = dataMessage - + let messageDescription = "🛟 Saved Canned Message Module Config for \(toUser.longName ?? NSLocalizedString("unknown", comment: "Unknown"))" - + if sendAdminMessageToRadio(meshPacket: meshPacket, adminDescription: messageDescription, fromUser: fromUser, toUser: toUser) { upsertCannedMessagesModuleConfigPacket(config: config, nodeNum: toUser.num, context: context!) return Int64(meshPacket.id) } - + return 0 } - + public func saveCannedMessageModuleMessages(messages: String, fromUser: UserEntity, toUser: UserEntity, adminIndex: Int32) -> Int64 { - + var adminPacket = AdminMessage() adminPacket.setCannedMessageModuleMessages = messages - + var meshPacket: MeshPacket = MeshPacket() meshPacket.to = UInt32(toUser.num) meshPacket.from = UInt32(fromUser.num) @@ -1307,22 +1316,22 @@ class BLEManager: NSObject, CBPeripheralDelegate, ObservableObject { dataMessage.portnum = PortNum.adminApp dataMessage.wantResponse = true meshPacket.decoded = dataMessage - + let messageDescription = "🛟 Saved Canned Message Module Messages for \(toUser.longName ?? NSLocalizedString("unknown", comment: "Unknown"))" - + if sendAdminMessageToRadio(meshPacket: meshPacket, adminDescription: messageDescription, fromUser: fromUser, toUser: toUser) { - + return Int64(meshPacket.id) } - + return 0 } - + public func saveExternalNotificationModuleConfig(config: ModuleConfig.ExternalNotificationConfig, fromUser: UserEntity, toUser: UserEntity, adminIndex: Int32) -> Int64 { - + var adminPacket = AdminMessage() adminPacket.setModuleConfig.externalNotification = config - + var meshPacket: MeshPacket = MeshPacket() meshPacket.to = UInt32(toUser.num) meshPacket.from = UInt32(fromUser.num) @@ -1330,12 +1339,12 @@ class BLEManager: NSObject, CBPeripheralDelegate, ObservableObject { meshPacket.id = UInt32.random(in: UInt32(UInt8.max).. Int64 { - + + public func saveRtttlConfig(ringtone: String, fromUser: UserEntity, toUser: UserEntity, adminIndex: Int32) -> Int64 { + var adminPacket = AdminMessage() - adminPacket.setModuleConfig.mqtt = config - + adminPacket.setRingtoneMessage = ringtone + var meshPacket: MeshPacket = MeshPacket() meshPacket.id = UInt32.random(in: UInt32(UInt8.max).. Int64 { + + var adminPacket = AdminMessage() + adminPacket.setModuleConfig.mqtt = config + + var meshPacket: MeshPacket = MeshPacket() + meshPacket.id = UInt32.random(in: UInt32(UInt8.max).. Int64 { - + var adminPacket = AdminMessage() adminPacket.setModuleConfig.rangeTest = config - + var meshPacket: MeshPacket = MeshPacket() meshPacket.id = UInt32.random(in: UInt32(UInt8.max).. Int64 { - + var adminPacket = AdminMessage() adminPacket.setModuleConfig.serial = config - + var meshPacket: MeshPacket = MeshPacket() meshPacket.id = UInt32.random(in: UInt32(UInt8.max).. Int64 { - + var adminPacket = AdminMessage() adminPacket.setModuleConfig.telemetry = config - + var meshPacket: MeshPacket = MeshPacket() meshPacket.id = UInt32.random(in: UInt32(UInt8.max).. Bool { - + var adminPacket = AdminMessage() adminPacket.getChannelRequest = channelIndex - + var meshPacket: MeshPacket = MeshPacket() meshPacket.to = UInt32(toUser.num) meshPacket.from = UInt32(fromUser.num) meshPacket.id = UInt32.random(in: UInt32(UInt8.max).. Bool { - + var adminPacket = AdminMessage() adminPacket.getCannedMessageModuleMessagesRequest = true - + var meshPacket: MeshPacket = MeshPacket() meshPacket.to = UInt32(destNum) meshPacket.from = UInt32(connectedPeripheral.num) @@ -1491,35 +1527,35 @@ class BLEManager: NSObject, CBPeripheralDelegate, ObservableObject { meshPacket.priority = MeshPacket.Priority.reliable meshPacket.wantAck = true meshPacket.decoded.wantResponse = wantResponse - + var dataMessage = DataMessage() dataMessage.payload = try! adminPacket.serializedData() dataMessage.portnum = PortNum.adminApp dataMessage.wantResponse = wantResponse - + meshPacket.decoded = dataMessage - + var toRadio: ToRadio! toRadio = ToRadio() toRadio.packet = meshPacket - + let binaryData: Data = try! toRadio.serializedData() - + if connectedPeripheral!.peripheral.state == CBPeripheralState.connected { connectedPeripheral.peripheral.writeValue(binaryData, for: TORADIO_characteristic, type: .withResponse) let logString = String.localizedStringWithFormat(NSLocalizedString("mesh.log.cannedmessages.messages.get %@", comment: "Requested Canned Messages Module Messages for node: %@"), String(connectedPeripheral.num)) MeshLogger.log("🥫 \(logString)") return true } - + 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) @@ -1527,27 +1563,27 @@ class BLEManager: NSObject, CBPeripheralDelegate, ObservableObject { meshPacket.priority = MeshPacket.Priority.reliable meshPacket.channel = UInt32(adminIndex) meshPacket.wantAck = true - + var dataMessage = DataMessage() dataMessage.payload = try! adminPacket.serializedData() dataMessage.portnum = PortNum.adminApp dataMessage.wantResponse = true - + meshPacket.decoded = dataMessage - + let messageDescription = "🛎️ Requested 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 requestDeviceConfig(fromUser: UserEntity, toUser: UserEntity, adminIndex: Int32) -> Bool { - + var adminPacket = AdminMessage() adminPacket.getConfigRequest = AdminMessage.ConfigType.deviceConfig - + var meshPacket: MeshPacket = MeshPacket() meshPacket.to = UInt32(toUser.num) meshPacket.from = UInt32(fromUser.num) @@ -1555,27 +1591,27 @@ class BLEManager: NSObject, CBPeripheralDelegate, ObservableObject { meshPacket.priority = MeshPacket.Priority.reliable meshPacket.channel = UInt32(adminIndex) meshPacket.wantAck = true - + var dataMessage = DataMessage() dataMessage.payload = try! adminPacket.serializedData() dataMessage.portnum = PortNum.adminApp dataMessage.wantResponse = true - + meshPacket.decoded = dataMessage - + let messageDescription = "🛎️ Requested Device 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 requestDisplayConfig(fromUser: UserEntity, toUser: UserEntity, adminIndex: Int32) -> Bool { - + var adminPacket = AdminMessage() adminPacket.getConfigRequest = AdminMessage.ConfigType.displayConfig - + var meshPacket: MeshPacket = MeshPacket() meshPacket.to = UInt32(toUser.num) meshPacket.from = UInt32(fromUser.num) @@ -1583,27 +1619,27 @@ class BLEManager: NSObject, CBPeripheralDelegate, ObservableObject { meshPacket.priority = MeshPacket.Priority.reliable meshPacket.channel = UInt32(adminIndex) meshPacket.wantAck = true - + var dataMessage = DataMessage() dataMessage.payload = try! adminPacket.serializedData() dataMessage.portnum = PortNum.adminApp dataMessage.wantResponse = true - + meshPacket.decoded = dataMessage - + let messageDescription = "🛎️ Requested Display 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) @@ -1611,29 +1647,29 @@ class BLEManager: NSObject, CBPeripheralDelegate, ObservableObject { meshPacket.priority = MeshPacket.Priority.reliable meshPacket.channel = UInt32(adminIndex) meshPacket.wantAck = true - + var dataMessage = DataMessage() dataMessage.payload = try! adminPacket.serializedData() dataMessage.portnum = PortNum.adminApp dataMessage.wantResponse = true - + meshPacket.decoded = dataMessage - + let messageDescription = "🛎️ Requested 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 requestNetworkConfig(fromUser: UserEntity, toUser: UserEntity, adminIndex: Int32) -> Bool { - + var adminPacket = AdminMessage() adminPacket.getConfigRequest = AdminMessage.ConfigType.networkConfig - + var meshPacket: MeshPacket = MeshPacket() meshPacket.to = UInt32(toUser.num) meshPacket.from = UInt32(fromUser.num) @@ -1641,26 +1677,26 @@ class BLEManager: NSObject, CBPeripheralDelegate, ObservableObject { meshPacket.priority = MeshPacket.Priority.reliable meshPacket.channel = UInt32(adminIndex) meshPacket.wantAck = true - + var dataMessage = DataMessage() dataMessage.payload = try! adminPacket.serializedData() dataMessage.portnum = PortNum.adminApp dataMessage.wantResponse = true meshPacket.decoded = dataMessage - + let messageDescription = "🛎️ Requested Network 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 requestPositionConfig(fromUser: UserEntity, toUser: UserEntity, adminIndex: Int32) -> Bool { - + var adminPacket = AdminMessage() adminPacket.getConfigRequest = AdminMessage.ConfigType.positionConfig - + var meshPacket: MeshPacket = MeshPacket() meshPacket.to = UInt32(toUser.num) meshPacket.from = UInt32(fromUser.num) @@ -1668,26 +1704,26 @@ class BLEManager: NSObject, CBPeripheralDelegate, ObservableObject { meshPacket.priority = MeshPacket.Priority.reliable meshPacket.channel = UInt32(adminIndex) meshPacket.wantAck = true - + var dataMessage = DataMessage() dataMessage.payload = try! adminPacket.serializedData() dataMessage.portnum = PortNum.adminApp dataMessage.wantResponse = true - + meshPacket.decoded = dataMessage - + let messageDescription = "🛎️ Requested Position 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 requestCannedMessagesModuleConfig(fromUser: UserEntity, toUser: UserEntity, adminIndex: Int32) -> Bool { - + var adminPacket = AdminMessage() adminPacket.getModuleConfigRequest = AdminMessage.ModuleConfigType.cannedmsgConfig - + var meshPacket: MeshPacket = MeshPacket() meshPacket.to = UInt32(toUser.num) meshPacket.from = UInt32(fromUser.num) @@ -1695,26 +1731,26 @@ class BLEManager: NSObject, CBPeripheralDelegate, ObservableObject { meshPacket.priority = MeshPacket.Priority.reliable meshPacket.channel = UInt32(adminIndex) meshPacket.wantAck = true - + var dataMessage = DataMessage() dataMessage.payload = try! adminPacket.serializedData() dataMessage.portnum = PortNum.adminApp dataMessage.wantResponse = true - + meshPacket.decoded = dataMessage - + let messageDescription = "🛎️ Requested Canned Messages Module 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 requestExternalNotificationModuleConfig(fromUser: UserEntity, toUser: UserEntity, adminIndex: Int32) -> Bool { - + var adminPacket = AdminMessage() adminPacket.getModuleConfigRequest = AdminMessage.ModuleConfigType.extnotifConfig - + var meshPacket: MeshPacket = MeshPacket() meshPacket.to = UInt32(toUser.num) meshPacket.from = UInt32(fromUser.num) @@ -1722,26 +1758,26 @@ class BLEManager: NSObject, CBPeripheralDelegate, ObservableObject { meshPacket.priority = MeshPacket.Priority.reliable meshPacket.channel = UInt32(adminIndex) meshPacket.wantAck = true - + var dataMessage = DataMessage() dataMessage.payload = try! adminPacket.serializedData() dataMessage.portnum = PortNum.adminApp dataMessage.wantResponse = true - + meshPacket.decoded = dataMessage - + let messageDescription = "🛎️ Requested External Notificaiton Module 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 requestRangeTestModuleConfig(fromUser: UserEntity, toUser: UserEntity, adminIndex: Int32) -> Bool { - + + public func requestRtttlConfig(fromUser: UserEntity, toUser: UserEntity, adminIndex: Int32) -> Bool { + var adminPacket = AdminMessage() - adminPacket.getModuleConfigRequest = AdminMessage.ModuleConfigType.rangetestConfig - + adminPacket.getRingtoneRequest = true + var meshPacket: MeshPacket = MeshPacket() meshPacket.to = UInt32(toUser.num) meshPacket.from = UInt32(fromUser.num) @@ -1749,26 +1785,53 @@ class BLEManager: NSObject, CBPeripheralDelegate, ObservableObject { meshPacket.priority = MeshPacket.Priority.reliable meshPacket.channel = UInt32(adminIndex) meshPacket.wantAck = true - + var dataMessage = DataMessage() dataMessage.payload = try! adminPacket.serializedData() dataMessage.portnum = PortNum.adminApp dataMessage.wantResponse = true - + meshPacket.decoded = dataMessage - + + let messageDescription = "🛎️ Requested RTTTL Ringtone 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 requestRangeTestModuleConfig(fromUser: UserEntity, toUser: UserEntity, adminIndex: Int32) -> Bool { + + var adminPacket = AdminMessage() + adminPacket.getModuleConfigRequest = AdminMessage.ModuleConfigType.rangetestConfig + + var meshPacket: MeshPacket = MeshPacket() + meshPacket.to = UInt32(toUser.num) + meshPacket.from = UInt32(fromUser.num) + meshPacket.id = UInt32.random(in: UInt32(UInt8.max).. Bool { - + var adminPacket = AdminMessage() adminPacket.getModuleConfigRequest = AdminMessage.ModuleConfigType.mqttConfig - + var meshPacket: MeshPacket = MeshPacket() meshPacket.to = UInt32(toUser.num) meshPacket.from = UInt32(fromUser.num) @@ -1776,26 +1839,26 @@ class BLEManager: NSObject, CBPeripheralDelegate, ObservableObject { meshPacket.priority = MeshPacket.Priority.reliable meshPacket.channel = UInt32(adminIndex) meshPacket.wantAck = true - + var dataMessage = DataMessage() dataMessage.payload = try! adminPacket.serializedData() dataMessage.portnum = PortNum.adminApp dataMessage.wantResponse = true - + meshPacket.decoded = dataMessage - + let messageDescription = "🛎️ Requested MQTT Module 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 requestSerialModuleConfig(fromUser: UserEntity, toUser: UserEntity, adminIndex: Int32) -> Bool { - + var adminPacket = AdminMessage() adminPacket.getModuleConfigRequest = AdminMessage.ModuleConfigType.serialConfig - + var meshPacket: MeshPacket = MeshPacket() meshPacket.to = UInt32(toUser.num) meshPacket.from = UInt32(fromUser.num) @@ -1803,26 +1866,26 @@ class BLEManager: NSObject, CBPeripheralDelegate, ObservableObject { meshPacket.priority = MeshPacket.Priority.reliable meshPacket.channel = UInt32(adminIndex) meshPacket.wantAck = true - + var dataMessage = DataMessage() dataMessage.payload = try! adminPacket.serializedData() dataMessage.portnum = PortNum.adminApp dataMessage.wantResponse = true - + meshPacket.decoded = dataMessage - + let messageDescription = "🛎️ Requested Serial Module 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 requestTelemetryModuleConfig(fromUser: UserEntity, toUser: UserEntity, adminIndex: Int32) -> Bool { - + var adminPacket = AdminMessage() adminPacket.getModuleConfigRequest = AdminMessage.ModuleConfigType.telemetryConfig - + var meshPacket: MeshPacket = MeshPacket() meshPacket.to = UInt32(toUser.num) meshPacket.from = UInt32(fromUser.num) @@ -1830,29 +1893,29 @@ class BLEManager: NSObject, CBPeripheralDelegate, ObservableObject { meshPacket.priority = MeshPacket.Priority.reliable meshPacket.channel = UInt32(adminIndex) meshPacket.wantAck = true - + var dataMessage = DataMessage() dataMessage.payload = try! adminPacket.serializedData() dataMessage.portnum = PortNum.adminApp dataMessage.wantResponse = true - + meshPacket.decoded = dataMessage - + let messageDescription = "🛎️ Requested Telemetry Module Config on admin channel \(adminIndex) for node: \(String(connectedPeripheral.num))" if sendAdminMessageToRadio(meshPacket: meshPacket, adminDescription: messageDescription, fromUser: fromUser, toUser: toUser) { return true } return false } - + // Send an admin message to a radio, save a message to core data for logging private func sendAdminMessageToRadio(meshPacket: MeshPacket, adminDescription: String, fromUser: UserEntity, toUser: UserEntity) -> Bool { - + var toRadio: ToRadio! toRadio = ToRadio() toRadio.packet = meshPacket let binaryData: Data = try! toRadio.serializedData() - + if connectedPeripheral!.peripheral.state == CBPeripheralState.connected { let newMessage = MessageEntity(context: context!) newMessage.messageId = Int64(meshPacket.id) @@ -1862,7 +1925,7 @@ class BLEManager: NSObject, CBPeripheralDelegate, ObservableObject { newMessage.adminDescription = adminDescription newMessage.fromUser = fromUser newMessage.toUser = toUser - + do { connectedPeripheral.peripheral.writeValue(binaryData, for: TORADIO_characteristic, type: .withResponse) try context!.save() @@ -1876,12 +1939,12 @@ class BLEManager: NSObject, CBPeripheralDelegate, ObservableObject { } return false } - + public func tryClearExistingChannels() { // Before we get started delete the existing channels from the myNodeInfo let fetchMyInfoRequest: NSFetchRequest = NSFetchRequest.init(entityName: "MyInfoEntity") fetchMyInfoRequest.predicate = NSPredicate(format: "myNodeNum == %lld", Int64(connectedPeripheral.num)) - + do { let fetchedMyInfo = try context?.fetch(fetchMyInfoRequest) as? [MyInfoEntity] ?? [] if fetchedMyInfo.count == 1 { @@ -1902,7 +1965,7 @@ class BLEManager: NSObject, CBPeripheralDelegate, ObservableObject { // MARK: - CB Central Manager implmentation extension BLEManager: CBCentralManagerDelegate { - + // MARK: Bluetooth enabled/disabled func centralManagerDidUpdateState(_ central: CBCentralManager) { if central.state == CBManagerState.poweredOn { @@ -1912,9 +1975,9 @@ extension BLEManager: CBCentralManagerDelegate { } else { isSwitchedOn = false } - + var status = "" - + switch central.state { case .poweredOff: status = "BLE is powered off" @@ -1933,10 +1996,10 @@ extension BLEManager: CBCentralManagerDelegate { } print("BLEManager status: \(status)") } - + // Called each time a peripheral is discovered func centralManager(_ central: CBCentralManager, didDiscover peripheral: CBPeripheral, advertisementData: [String: Any], rssi RSSI: NSNumber) { - + if self.automaticallyReconnect && timeoutTimerRuns < 2 && peripheral.identifier.uuidString == UserDefaults.standard.object(forKey: "preferredPeripheralId") as? String ?? "" { self.connectTo(peripheral: peripheral) print("ℹ️ BLE Reconnecting to prefered peripheral: \(peripheral.name ?? "Unknown")") @@ -1944,7 +2007,7 @@ extension BLEManager: CBCentralManagerDelegate { let name = advertisementData[CBAdvertisementDataLocalNameKey] as? String let device = Peripheral(id: peripheral.identifier.uuidString, num: 0, name: name ?? "Unknown", shortName: "????", longName: name ?? "Unknown", firmwareVersion: "Unknown", rssi: RSSI.intValue, lastUpdate: Date(), peripheral: peripheral) let index = peripherals.map { $0.peripheral }.firstIndex(of: peripheral) - + if let peripheralIndex = index { peripherals[peripheralIndex] = device } else { @@ -1954,29 +2017,29 @@ extension BLEManager: CBCentralManagerDelegate { let visibleDuration = Calendar.current.date(byAdding: .second, value: -5, to: today)! self.peripherals.removeAll(where: { $0.lastUpdate < visibleDuration}) } - -// func centralManager(_ central: CBCentralManager, willRestoreState dict: [String : Any]) { -// -// guard let peripherals = dict[CBCentralManagerRestoredStatePeripheralsKey] as? [CBPeripheral] else { -// return -// } -// -// if peripherals.count > 0 { -// -// for peripheral in peripherals { -// print(peripheral) -// switch peripheral.state { -// case .connecting: // I've only seen this happen when -// // re-launching attached to Xcode. -// print("Xcode Restore") -// -// case .connected: -// connectTo(peripheral: peripheral) -// print("Restore BLE State") -// default: break -// } -// } -// } -// print("willRestoreState Hit!") -// } + + // func centralManager(_ central: CBCentralManager, willRestoreState dict: [String : Any]) { + // + // guard let peripherals = dict[CBCentralManagerRestoredStatePeripheralsKey] as? [CBPeripheral] else { + // return + // } + // + // if peripherals.count > 0 { + // + // for peripheral in peripherals { + // print(peripheral) + // switch peripheral.state { + // case .connecting: // I've only seen this happen when + // // re-launching attached to Xcode. + // print("Xcode Restore") + // + // case .connected: + // connectTo(peripheral: peripheral) + // print("Restore BLE State") + // default: break + // } + // } + // } + // print("willRestoreState Hit!") + // } } diff --git a/Meshtastic/Helpers/MeshPackets.swift b/Meshtastic/Helpers/MeshPackets.swift index d329dceb..597d88d8 100644 --- a/Meshtastic/Helpers/MeshPackets.swift +++ b/Meshtastic/Helpers/MeshPackets.swift @@ -279,6 +279,7 @@ func nodeInfoPacket (nodeInfo: NodeInfo, channel: UInt32, context: NSManagedObje if nodeInfo.position.longitudeI > 0 || nodeInfo.position.latitudeI > 0 && (nodeInfo.position.latitudeI != 373346000 && nodeInfo.position.longitudeI != -1220090000) { let position = PositionEntity(context: context) + position.latest = true position.seqNo = Int32(nodeInfo.position.seqNumber) position.latitudeI = nodeInfo.position.latitudeI position.longitudeI = nodeInfo.position.longitudeI @@ -486,6 +487,9 @@ func adminAppPacket (packet: MeshPacket, context: NSManagedObjectContext) { } + } else if adminMessage.payloadVariant == AdminMessage.OneOf_PayloadVariant.getRingtoneResponse(adminMessage.getRingtoneResponse) { + let ringtone = adminMessage.getRingtoneResponse + upsertRtttlConfigPacket(ringtone: ringtone, nodeNum: Int64(packet.from), context: context) } else { MeshLogger.log("🕸️ MESH PACKET received for Admin App \(try! packet.decoded.jsonString())") } diff --git a/Meshtastic/Meshtastic.xcdatamodeld/.xccurrentversion b/Meshtastic/Meshtastic.xcdatamodeld/.xccurrentversion index 6e12995e..ff97f8c7 100644 --- a/Meshtastic/Meshtastic.xcdatamodeld/.xccurrentversion +++ b/Meshtastic/Meshtastic.xcdatamodeld/.xccurrentversion @@ -3,6 +3,6 @@ _XCCurrentVersionName - MeshtasticDataModelV9.xcdatamodel + MeshtasticDataModelV10.xcdatamodel diff --git a/Meshtastic/Meshtastic.xcdatamodeld/MeshtasticDataModelV10.xcdatamodel/contents b/Meshtastic/Meshtastic.xcdatamodeld/MeshtasticDataModelV10.xcdatamodel/contents new file mode 100644 index 00000000..bd6cce58 --- /dev/null +++ b/Meshtastic/Meshtastic.xcdatamodeld/MeshtasticDataModelV10.xcdatamodel/contents @@ -0,0 +1,313 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/Meshtastic/Persistence/Persistence.swift b/Meshtastic/Persistence/Persistence.swift index d4a86742..1c768ccf 100644 --- a/Meshtastic/Persistence/Persistence.swift +++ b/Meshtastic/Persistence/Persistence.swift @@ -1,8 +1,8 @@ // // Persistence.swift -// CoreDataSample +// Meshtastic // -// Created by Garth Vander Houwen on 11/28/21. +// Copyright(c) Garth Vander Houwen 11/28/21. // import CoreData diff --git a/Meshtastic/Persistence/PositionEntityExtension.swift b/Meshtastic/Persistence/PositionEntityExtension.swift index b671e6e8..75ba3f4b 100644 --- a/Meshtastic/Persistence/PositionEntityExtension.swift +++ b/Meshtastic/Persistence/PositionEntityExtension.swift @@ -1,3 +1,10 @@ +// +// PersistenceEntityExtenstion.swift +// Meshtastic +// +// Copyright(c) Garth Vander Houwen 11/28/21. +// + import CoreData import CoreLocation import MapKit diff --git a/Meshtastic/Persistence/UpdateCoreData.swift b/Meshtastic/Persistence/UpdateCoreData.swift index 9763b0e7..d881e082 100644 --- a/Meshtastic/Persistence/UpdateCoreData.swift +++ b/Meshtastic/Persistence/UpdateCoreData.swift @@ -535,19 +535,27 @@ func upsertPositionConfigPacket(config: Meshtastic.Config.PositionConfig, nodeNu let newPositionConfig = PositionConfigEntity(context: context) newPositionConfig.smartPositionEnabled = config.positionBroadcastSmartEnabled newPositionConfig.deviceGpsEnabled = config.gpsEnabled + newPositionConfig.rxGpio = Int32(config.rxGpio) + newPositionConfig.txGpio = Int32(config.txGpio) newPositionConfig.fixedPosition = config.fixedPosition newPositionConfig.gpsUpdateInterval = Int32(config.gpsUpdateInterval) newPositionConfig.gpsAttemptTime = Int32(config.gpsAttemptTime) newPositionConfig.positionBroadcastSeconds = Int32(config.positionBroadcastSecs) + newPositionConfig.broadcastSmartMinimumIntervalSecs = Int32(config.broadcastSmartMinimumIntervalSecs) + newPositionConfig.broadcastSmartMinimumDistance = Int32(config.broadcastSmartMinimumDistance) newPositionConfig.positionFlags = Int32(config.positionFlags) fetchedNode[0].positionConfig = newPositionConfig } else { fetchedNode[0].positionConfig?.smartPositionEnabled = config.positionBroadcastSmartEnabled fetchedNode[0].positionConfig?.deviceGpsEnabled = config.gpsEnabled + fetchedNode[0].positionConfig?.rxGpio = Int32(config.rxGpio) + fetchedNode[0].positionConfig?.txGpio = Int32(config.txGpio) fetchedNode[0].positionConfig?.fixedPosition = config.fixedPosition fetchedNode[0].positionConfig?.gpsUpdateInterval = Int32(config.gpsUpdateInterval) fetchedNode[0].positionConfig?.gpsAttemptTime = Int32(config.gpsAttemptTime) fetchedNode[0].positionConfig?.positionBroadcastSeconds = Int32(config.positionBroadcastSecs) + fetchedNode[0].positionConfig?.broadcastSmartMinimumIntervalSecs = Int32(config.broadcastSmartMinimumIntervalSecs) + fetchedNode[0].positionConfig?.broadcastSmartMinimumDistance = Int32(config.broadcastSmartMinimumDistance) fetchedNode[0].positionConfig?.positionFlags = Int32(config.positionFlags) } do { @@ -699,6 +707,45 @@ func upsertExternalNotificationModuleConfigPacket(config: Meshtastic.ModuleConfi } } +func upsertRtttlConfigPacket(ringtone: String, nodeNum: Int64, context: NSManagedObjectContext) { + + let logString = String.localizedStringWithFormat(NSLocalizedString("mesh.log.ringtone.config %@", comment: "RTTTL Ringtone config received: %@"), String(nodeNum)) + MeshLogger.log("⛰️ \(logString)") + + let fetchNodeInfoRequest: NSFetchRequest = NSFetchRequest.init(entityName: "NodeInfoEntity") + fetchNodeInfoRequest.predicate = NSPredicate(format: "num == %lld", Int64(nodeNum)) + + do { + + guard let fetchedNode = try context.fetch(fetchNodeInfoRequest) as? [NodeInfoEntity] else { + return + } + // Found a node, save RTTTL Config + if !fetchedNode.isEmpty { + if fetchedNode[0].rtttlConfig == nil { + let newRtttlConfig = RTTTLConfigEntity(context: context) + newRtttlConfig.ringtone = ringtone + fetchedNode[0].rtttlConfig = newRtttlConfig + } else { + fetchedNode[0].rtttlConfig?.ringtone = ringtone + } + do { + try context.save() + print("💾 Updated RTTTL Ringtone Config for node number: \(String(nodeNum))") + } catch { + context.rollback() + let nsError = error as NSError + print("💥 Error Updating Core Data RtttlConfigEntity: \(nsError)") + } + } else { + print("💥 No Nodes found in local database matching node number \(nodeNum) unable to save RTTTL Ringtone Config") + } + } catch { + let nsError = error as NSError + print("💥 Fetching node for core data RtttlConfigEntity failed: \(nsError)") + } +} + func upsertMqttModuleConfigPacket(config: Meshtastic.ModuleConfig.MQTTConfig, nodeNum: Int64, context: NSManagedObjectContext) { let logString = String.localizedStringWithFormat(NSLocalizedString("mesh.log.mqtt.config %@", comment: "MQTT module config received: %@"), String(nodeNum)) @@ -719,7 +766,7 @@ func upsertMqttModuleConfigPacket(config: Meshtastic.ModuleConfig.MQTTConfig, no let newMQTTConfig = MQTTConfigEntity(context: context) newMQTTConfig.enabled = config.enabled newMQTTConfig.address = config.address - newMQTTConfig.address = config.username + newMQTTConfig.username = config.username newMQTTConfig.password = config.password newMQTTConfig.encryptionEnabled = config.encryptionEnabled newMQTTConfig.jsonEnabled = config.jsonEnabled @@ -727,7 +774,7 @@ func upsertMqttModuleConfigPacket(config: Meshtastic.ModuleConfig.MQTTConfig, no } else { fetchedNode[0].mqttConfig?.enabled = config.enabled fetchedNode[0].mqttConfig?.address = config.address - fetchedNode[0].mqttConfig?.address = config.username + fetchedNode[0].mqttConfig?.username = config.username fetchedNode[0].mqttConfig?.password = config.password fetchedNode[0].mqttConfig?.encryptionEnabled = config.encryptionEnabled fetchedNode[0].mqttConfig?.jsonEnabled = config.jsonEnabled diff --git a/Meshtastic/Persistence/UserEntityExtension.swift b/Meshtastic/Persistence/UserEntityExtension.swift index 1d2dd3da..6d9f5699 100644 --- a/Meshtastic/Persistence/UserEntityExtension.swift +++ b/Meshtastic/Persistence/UserEntityExtension.swift @@ -1,6 +1,6 @@ // // UserEntityExtension.swift -// MeshtasticApple +// Meshtastic // // Copyright(c) Garth Vander Houwen 6/3/22. // diff --git a/Meshtastic/Protobufs/meshtastic/config.pb.swift b/Meshtastic/Protobufs/meshtastic/config.pb.swift index 7be86a36..4f92bedd 100644 --- a/Meshtastic/Protobufs/meshtastic/config.pb.swift +++ b/Meshtastic/Protobufs/meshtastic/config.pb.swift @@ -354,6 +354,14 @@ struct Config { /// (Re)define GPS_TX_PIN for your board. var txGpio: UInt32 = 0 + /// + /// The minimum distance in meters traveled (since the last send) before we can send a position to the mesh if position_broadcast_smart_enabled + var broadcastSmartMinimumDistance: UInt32 = 0 + + /// + /// The minumum number of seconds (since the last send) before we can send a position to the mesh if position_broadcast_smart_enabled + var broadcastSmartMinimumIntervalSecs: UInt32 = 0 + var unknownFields = SwiftProtobuf.UnknownStorage() /// @@ -1669,6 +1677,8 @@ extension Config.PositionConfig: SwiftProtobuf.Message, SwiftProtobuf._MessageIm 7: .standard(proto: "position_flags"), 8: .standard(proto: "rx_gpio"), 9: .standard(proto: "tx_gpio"), + 10: .standard(proto: "broadcast_smart_minimum_distance"), + 11: .standard(proto: "broadcast_smart_minimum_interval_secs"), ] mutating func decodeMessage(decoder: inout D) throws { @@ -1686,6 +1696,8 @@ extension Config.PositionConfig: SwiftProtobuf.Message, SwiftProtobuf._MessageIm case 7: try { try decoder.decodeSingularUInt32Field(value: &self.positionFlags) }() case 8: try { try decoder.decodeSingularUInt32Field(value: &self.rxGpio) }() case 9: try { try decoder.decodeSingularUInt32Field(value: &self.txGpio) }() + case 10: try { try decoder.decodeSingularUInt32Field(value: &self.broadcastSmartMinimumDistance) }() + case 11: try { try decoder.decodeSingularUInt32Field(value: &self.broadcastSmartMinimumIntervalSecs) }() default: break } } @@ -1719,6 +1731,12 @@ extension Config.PositionConfig: SwiftProtobuf.Message, SwiftProtobuf._MessageIm if self.txGpio != 0 { try visitor.visitSingularUInt32Field(value: self.txGpio, fieldNumber: 9) } + if self.broadcastSmartMinimumDistance != 0 { + try visitor.visitSingularUInt32Field(value: self.broadcastSmartMinimumDistance, fieldNumber: 10) + } + if self.broadcastSmartMinimumIntervalSecs != 0 { + try visitor.visitSingularUInt32Field(value: self.broadcastSmartMinimumIntervalSecs, fieldNumber: 11) + } try unknownFields.traverse(visitor: &visitor) } @@ -1732,6 +1750,8 @@ extension Config.PositionConfig: SwiftProtobuf.Message, SwiftProtobuf._MessageIm if lhs.positionFlags != rhs.positionFlags {return false} if lhs.rxGpio != rhs.rxGpio {return false} if lhs.txGpio != rhs.txGpio {return false} + if lhs.broadcastSmartMinimumDistance != rhs.broadcastSmartMinimumDistance {return false} + if lhs.broadcastSmartMinimumIntervalSecs != rhs.broadcastSmartMinimumIntervalSecs {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 7fa22878..03b6c4d1 100644 --- a/Meshtastic/Protobufs/meshtastic/mesh.pb.swift +++ b/Meshtastic/Protobufs/meshtastic/mesh.pb.swift @@ -1587,6 +1587,10 @@ struct NodeInfo { /// Clears the value of `deviceMetrics`. Subsequent reads from it will return its default value. mutating func clearDeviceMetrics() {self._deviceMetrics = nil} + /// + /// local channel index we heard that node on. Only populated if its not the default channel. + var channel: UInt32 = 0 + var unknownFields = SwiftProtobuf.UnknownStorage() init() {} @@ -3181,6 +3185,7 @@ extension NodeInfo: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationB 4: .same(proto: "snr"), 5: .standard(proto: "last_heard"), 6: .standard(proto: "device_metrics"), + 7: .same(proto: "channel"), ] mutating func decodeMessage(decoder: inout D) throws { @@ -3195,6 +3200,7 @@ extension NodeInfo: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationB case 4: try { try decoder.decodeSingularFloatField(value: &self.snr) }() case 5: try { try decoder.decodeSingularFixed32Field(value: &self.lastHeard) }() case 6: try { try decoder.decodeSingularMessageField(value: &self._deviceMetrics) }() + case 7: try { try decoder.decodeSingularUInt32Field(value: &self.channel) }() default: break } } @@ -3223,6 +3229,9 @@ extension NodeInfo: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationB try { if let v = self._deviceMetrics { try visitor.visitSingularMessageField(value: v, fieldNumber: 6) } }() + if self.channel != 0 { + try visitor.visitSingularUInt32Field(value: self.channel, fieldNumber: 7) + } try unknownFields.traverse(visitor: &visitor) } @@ -3233,6 +3242,7 @@ extension NodeInfo: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationB if lhs.snr != rhs.snr {return false} if lhs.lastHeard != rhs.lastHeard {return false} if lhs._deviceMetrics != rhs._deviceMetrics {return false} + if lhs.channel != rhs.channel {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 58711e4e..57594b2f 100644 --- a/Meshtastic/Views/Bluetooth/Connect.swift +++ b/Meshtastic/Views/Bluetooth/Connect.swift @@ -26,6 +26,20 @@ struct Connect: View { @State var presentingSwitchPreferredPeripheral = false @State var selectedPeripherialId = "" + init () { + let notificationCenter = UNUserNotificationCenter.current() + notificationCenter.getNotificationSettings(completionHandler: { (settings) in + if settings.authorizationStatus == .notDetermined { + UNUserNotificationCenter.current().requestAuthorization(options: [.alert, .badge, .sound]) { success, error in + if success { + print("Notifications are all set!") + } else if let error = error { + print(error.localizedDescription) + } + } + } + }) + } var body: some View { NavigationStack { @@ -90,7 +104,7 @@ struct Connect: View { #endif } } label: { - Label("Mesh Live Activity", systemImage: liveActivityStarted ? "stop" : "play") + Label("mesh.live.activity", systemImage: liveActivityStarted ? "stop" : "play") } } #endif @@ -279,18 +293,9 @@ struct Connect: View { .onAppear(perform: { self.bleManager.context = context self.bleManager.userSettings = userSettings - - // Ask for notification permission - UNUserNotificationCenter.current().requestAuthorization(options: [.alert, .badge, .sound]) { success, error in - if success { - print("Notifications are all set!") - } else if let error = error { - print(error.localizedDescription) - } - } }) } -#if canImport(ActivityKit) + #if canImport(ActivityKit) func startNodeActivity() { if #available(iOS 16.2, *) { liveActivityStarted = true @@ -330,29 +335,7 @@ struct Connect: View { } } } -#endif - -#if os(iOS) - func postNotification() { - let timerSeconds = 60 - let content = UNMutableNotificationContent() - content.title = "Mesh Live Activity Over" - content.body = "Your timed mesh live activity is over." - let trigger = UNTimeIntervalNotificationTrigger(timeInterval: TimeInterval(timerSeconds), repeats: false) - let uuidString = UUID().uuidString - let request = UNNotificationRequest(identifier: uuidString, - content: content, trigger: trigger) - let notificationCenter = UNUserNotificationCenter.current() - notificationCenter.add(request) { (error) in - if error != nil { - // Handle any errors. - print("Error posting local notification: \(error?.localizedDescription ?? "no description")") - } else { - print("Posted local notification.") - } - } - } -#endif + #endif func didDismissSheet() { bleManager.disconnectPeripheral(reconnect: false) diff --git a/Meshtastic/Views/Map/Custom/MapViewSwiftUI.swift b/Meshtastic/Views/Map/Custom/MapViewSwiftUI.swift index 2fb8b422..86f8c0a8 100644 --- a/Meshtastic/Views/Map/Custom/MapViewSwiftUI.swift +++ b/Meshtastic/Views/Map/Custom/MapViewSwiftUI.swift @@ -12,45 +12,51 @@ func degreesToRadians(_ number: Double) -> Double { } struct MapViewSwiftUI: UIViewRepresentable { - + var onLongPress: (_ waypointCoordinate: CLLocationCoordinate2D) -> Void var onWaypointEdit: (_ waypointId: Int ) -> Void let mapView = MKMapView() + let lineColors: [UIColor] = [UIColor.systemIndigo, UIColor.yellow, UIColor.white, UIColor.red, UIColor.purple, UIColor.orange, UIColor.magenta, UIColor.lightGray, UIColor.green, UIColor.gray, UIColor.systemMint, UIColor.darkGray, UIColor.cyan, UIColor.brown, UIColor.blue, UIColor.black, UIColor.systemPink, + UIColor.systemTeal] + // Parameters let positions: [PositionEntity] let waypoints: [WaypointEntity] let mapViewType: MKMapType let userTrackingMode: MKUserTrackingMode let showNodeHistory: Bool let showRouteLines: Bool - let colors: [UIColor] = [UIColor.systemIndigo, UIColor.orange, UIColor.green, UIColor.brown, UIColor.purple, UIColor.systemMint, UIColor.cyan, UIColor.magenta, UIColor.systemPink, UIColor.blue] - @AppStorage("meshMapRecentering") private var recenter: Bool = false - // Offline Maps - // make this view dependent on the UserDefault that is updated when importing a new map file + // Offline Map Tiles @AppStorage("lastUpdatedLocalMapFile") private var lastUpdatedLocalMapFile = 0 @State private var loadedLastUpdatedLocalMapFile = 0 var customMapOverlay: CustomMapOverlay? @State private var presentCustomMapOverlayHash: CustomMapOverlay? - func makeUIView(context: Context) -> MKMapView { // Map View Parameters mapView.mapType = mapViewType mapView.addAnnotations(waypoints) // Do the initial map centering + let latest = positions + .filter { $0.latest == true } + .sorted { $0.nodePosition?.num ?? 0 > $1.nodePosition?.num ?? -1 } let span = MKCoordinateSpan(latitudeDelta: 0.003, longitudeDelta: 0.003) - let center = LocationHelper.currentLocation + let center = (latest.count > 0 && userTrackingMode == MKUserTrackingMode.none) ? latest[0].coordinate : LocationHelper.currentLocation let region = MKCoordinateRegion(center: center, span: span) + mapView.addAnnotations(showNodeHistory ? positions : latest) mapView.setRegion(region, animated: true) // Set user (phone gps) tracking options - let latest = positions.filter { $0.latest == true } mapView.setUserTrackingMode(userTrackingMode, animated: true) if userTrackingMode == MKUserTrackingMode.none { + if latest.count == 1 { + mapView.fit(annotations:showNodeHistory ? positions : latest, andShow: false) + } else { + mapView.fitAllAnnotations() + } mapView.showsUserLocation = false } else { mapView.showsUserLocation = true } - mapView.addAnnotations(showNodeHistory ? positions : latest) // Other MKMapView Settings mapView.preferredConfiguration.elevationStyle = .realistic// .flat mapView.isPitchEnabled = true @@ -60,14 +66,14 @@ struct MapViewSwiftUI: UIViewRepresentable { mapView.showsBuildings = true mapView.showsScale = true mapView.showsTraffic = true - + #if targetEnvironment(macCatalyst) // Show the default always visible compass and the mac only controls mapView.showsCompass = true mapView.showsZoomControls = true 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 @@ -78,19 +84,21 @@ struct MapViewSwiftUI: UIViewRepresentable { 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 } - + 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 @@ -109,12 +117,15 @@ struct MapViewSwiftUI: UIViewRepresentable { self.loadedLastUpdatedLocalMapFile = self.lastUpdatedLocalMapFile } } - - DispatchQueue.main.async { + + // DispatchQueue.main.async { let latest = positions .filter { $0.latest == true } .sorted { $0.nodePosition?.num ?? 0 > $1.nodePosition?.num ?? -1 } - + let annotationCount = waypoints.count + (showNodeHistory ? positions.count : latest.count) + print("Annotation Count: \(annotationCount) Map Annotations: \(mapView.annotations.count)") + mapView.removeAnnotations(mapView.annotations) + mapView.addAnnotations(waypoints) if showRouteLines { // Remove all existing PolyLine Overlays for overlay in mapView.overlays { @@ -125,7 +136,7 @@ struct MapViewSwiftUI: UIViewRepresentable { var lineIndex = 0 for position in latest { - let nodePositions = positions.filter { $0.time! >= Calendar.current.startOfDay(for: Date()) && $0.nodePosition?.num ?? 0 == position.nodePosition?.num ?? -1 } + let nodePositions = positions.filter { $0.nodePosition?.num ?? 0 == position.nodePosition?.num ?? -1 } let lineCoords = nodePositions.map ({ (position) -> CLLocationCoordinate2D in return position.nodeCoordinate! @@ -134,47 +145,36 @@ struct MapViewSwiftUI: UIViewRepresentable { polyline.title = "\(String(position.nodePosition?.num ?? 0))-\(String(lineIndex))" mapView.addOverlay(polyline) lineIndex += 1 - // There are 10 colors for lines, start over if we are at index 10 - if lineIndex > 9 { + // There are 18 colors for lines, start over if we are at index 17 + if lineIndex > 17 { lineIndex = 0 } } } - - let annotationCount = waypoints.count + positions.count - if annotationCount != mapView.annotations.count { - mapView.removeAnnotations(mapView.annotations) - mapView.addAnnotations(waypoints) - mapView.setUserTrackingMode(userTrackingMode, animated: true) - - if userTrackingMode == MKUserTrackingMode.none { - mapView.showsUserLocation = false - mapView.addAnnotations(showNodeHistory ? positions : latest) - if recenter { - if showRouteLines || showNodeHistory { - mapView.fit(annotations: showNodeHistory ? positions : positions, andShow: false) - } else { - mapView.fitAllAnnotations() - } - } - } else { - // Centering Done by tracking mode - mapView.addAnnotations(showNodeHistory ? positions : latest) - mapView.showsUserLocation = true + if userTrackingMode == MKUserTrackingMode.none { + mapView.showsUserLocation = false + mapView.addAnnotations(showNodeHistory ? positions : latest) + if recenter { + mapView.fit(annotations:showNodeHistory || showRouteLines ? positions : latest, andShow: false) } + } else { + // Centering Done by tracking mode + mapView.addAnnotations(showNodeHistory ? positions : latest) + mapView.showsUserLocation = true } - } + mapView.setUserTrackingMode(userTrackingMode, animated: true) + //} } - + func makeCoordinator() -> MapCoordinator { return Coordinator(self) } - + final class MapCoordinator: NSObject, MKMapViewDelegate, UIGestureRecognizerDelegate { - + var parent: MapViewSwiftUI var longPressRecognizer = UILongPressGestureRecognizer() - + init(_ parent: MapViewSwiftUI) { self.parent = parent super.init() @@ -184,16 +184,16 @@ struct MapViewSwiftUI: UIViewRepresentable { self.longPressRecognizer.delegate = self self.parent.mapView.addGestureRecognizer(longPressRecognizer) } - + func mapView(_ mapView: MKMapView, viewFor annotation: MKAnnotation) -> MKAnnotationView? { - + switch annotation { case let positionAnnotation as PositionEntity: 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 - + if positionAnnotation.latest { annotationView.markerTintColor = .systemRed annotationView.displayPriority = .required @@ -216,7 +216,7 @@ struct MapViewSwiftUI: UIViewRepresentable { 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 { @@ -230,7 +230,7 @@ struct MapViewSwiftUI: UIViewRepresentable { } else if DeviceRoles(rawValue: Int(positionAnnotation.nodePosition!.metadata?.role ?? 0)) == DeviceRoles.sensor { annotationView.glyphImage = UIImage(systemName: "sensor") } - + let pf = PositionFlags(rawValue: Int(positionAnnotation.nodePosition?.metadata?.positionFlags ?? 3)) if pf.contains(.Satsinview) { subtitle.text! += "Sats in view: \(String(positionAnnotation.satsInView)) \n" @@ -239,7 +239,7 @@ struct MapViewSwiftUI: UIViewRepresentable { subtitle.text! += "Sequence: \(String(positionAnnotation.seqNo)) \n" } if pf.contains(.Heading) { - + if parent.userTrackingMode != MKUserTrackingMode.followWithHeading { annotationView.glyphImage = UIImage(systemName: "location.north.fill")?.rotate(radians: Float(degreesToRadians(Double(positionAnnotation.heading)))) subtitle.text! += "Heading: \(String(positionAnnotation.heading)) \n" @@ -255,7 +255,7 @@ struct MapViewSwiftUI: UIViewRepresentable { } subtitle.text! += "Speed: \(formatter.string(from: Measurement(value: Double(positionAnnotation.speed), unit: UnitSpeed.kilometersPerHour))) \n" } - + } else { // node metadata is nil annotationView.glyphImage = UIImage(systemName: "flipphone") @@ -316,23 +316,23 @@ struct MapViewSwiftUI: UIViewRepresentable { 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() @@ -343,9 +343,9 @@ struct MapViewSwiftUI: UIViewRepresentable { parent.onLongPress(coordinate) } } - + public func mapView(_ mapView: MKMapView, rendererFor overlay: MKOverlay) -> MKOverlayRenderer { - + if let tileOverlay = overlay as? MKTileOverlay { return MKTileOverlayRenderer(tileOverlay: tileOverlay) } else { @@ -354,26 +354,26 @@ struct MapViewSwiftUI: UIViewRepresentable { let titleString = routePolyline.title ?? "None-0" let index = Int(titleString.components(separatedBy: "-").last ?? "0") let renderer = MKPolylineRenderer(polyline: routePolyline) - renderer.strokeColor = parent.colors[index ?? 0] - renderer.lineWidth = 5 + renderer.strokeColor = parent.lineColors[index ?? 0] + renderer.lineWidth = 8 return renderer } 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 @@ -381,7 +381,7 @@ struct MapViewSwiftUI: UIViewRepresentable { var minimumZoomLevel: Int? var maximumZoomLevel: Int? let defaultTile: DefaultTile? - + public init( mapName: String, tileType: String, @@ -397,7 +397,7 @@ struct MapViewSwiftUI: UIViewRepresentable { self.maximumZoomLevel = maximumZoomLevel self.defaultTile = defaultTile } - + public init?( mapName: String?, tileType: String, @@ -417,15 +417,15 @@ struct MapViewSwiftUI: UIViewRepresentable { 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, @@ -438,7 +438,7 @@ struct MapViewSwiftUI: UIViewRepresentable { self.defaultTile = defaultTile super.init(urlTemplate: "") } - + public override func url(forTilePath path: MKTileOverlayPath) -> URL { if let tileUrl = Bundle.main.url( forResource: "\(path.y)", @@ -460,31 +460,4 @@ struct MapViewSwiftUI: UIViewRepresentable { } } } - -// 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 -// } -// } } diff --git a/Meshtastic/Views/Messages/ChannelMessageList.swift b/Meshtastic/Views/Messages/ChannelMessageList.swift index 531c3672..3297f28f 100644 --- a/Meshtastic/Views/Messages/ChannelMessageList.swift +++ b/Meshtastic/Views/Messages/ChannelMessageList.swift @@ -322,7 +322,7 @@ struct ChannelMessageList: View { focusedField = nil replyMessageId = 0 if sendPositionWithMessage { - if bleManager.sendPosition(destNum: Int64(channel.index), wantResponse: false) { + if bleManager.sendPosition(destNum: Int64(channel.index), wantResponse: false, smartPosition: false) { print("Location Sent") } } @@ -339,7 +339,7 @@ struct ChannelMessageList: View { focusedField = nil replyMessageId = 0 if sendPositionWithMessage { - if bleManager.sendPosition(destNum: Int64(channel.index), wantResponse: false) { + if bleManager.sendPosition(destNum: Int64(channel.index), wantResponse: false, smartPosition: false) { print("Location Sent") } } diff --git a/Meshtastic/Views/Messages/UserMessageList.swift b/Meshtastic/Views/Messages/UserMessageList.swift index 0abcb0b9..ee6ee822 100644 --- a/Meshtastic/Views/Messages/UserMessageList.swift +++ b/Meshtastic/Views/Messages/UserMessageList.swift @@ -133,7 +133,7 @@ struct UserMessageList: View { let ackDate = Date(timeIntervalSince1970: TimeInterval(message.ackTimestamp)) let sixMonthsAgo = Calendar.current.date(byAdding: .month, value: -6, to: Date()) if ackDate >= sixMonthsAgo! { - Text("Ack Time: \(ackDate.formattedDate(format: "h:mm:ss a"))").foregroundColor(.gray) + Text("Ack Time: \(ackDate.formattedDate(format: "h:mm:ss.SSSS a"))").foregroundColor(.gray) } else { Text("unknown.age").font(.caption2).foregroundColor(.gray) } @@ -328,7 +328,7 @@ struct UserMessageList: View { focusedField = nil replyMessageId = 0 if sendPositionWithMessage { - if bleManager.sendPosition(destNum: user.num, wantResponse: true) { + if bleManager.sendPosition(destNum: user.num, wantResponse: true, smartPosition: false) { print("Location Sent") } } @@ -345,7 +345,7 @@ struct UserMessageList: View { focusedField = nil replyMessageId = 0 if sendPositionWithMessage { - if bleManager.sendPosition(destNum: user.num, wantResponse: true) { + if bleManager.sendPosition(destNum: user.num, wantResponse: true, smartPosition: false) { print("Location Sent") } } diff --git a/Meshtastic/Views/Nodes/DeviceMetricsLog.swift b/Meshtastic/Views/Nodes/DeviceMetricsLog.swift index 98ba306b..3ed3ad81 100644 --- a/Meshtastic/Views/Nodes/DeviceMetricsLog.swift +++ b/Meshtastic/Views/Nodes/DeviceMetricsLog.swift @@ -105,7 +105,7 @@ struct DeviceMetricsLog: View { ForEach(node.telemetries?.reversed() as? [TelemetryEntity] ?? [], id: \.self) { (dm: TelemetryEntity) in if dm.metricsType == 0 { GridRow { - if dm.batteryLevel == 0 { + if dm.batteryLevel == 111 { Text("USB") .font(.caption) } else { diff --git a/Meshtastic/Views/Nodes/NodeDetail.swift b/Meshtastic/Views/Nodes/NodeDetail.swift index 38544cde..24634d6c 100644 --- a/Meshtastic/Views/Nodes/NodeDetail.swift +++ b/Meshtastic/Views/Nodes/NodeDetail.swift @@ -60,7 +60,8 @@ struct NodeDetail: View { if node.positions?.count ?? 0 > 0 { ZStack { let positionArray = node.positions?.array as? [PositionEntity] ?? [] - let todaysPositions = positionArray.filter { $0.time! >= Calendar.current.startOfDay(for: Date()) } + let lastTenThousand = Array(positionArray.prefix(10000)) + // let todaysPositions = positionArray.filter { $0.time! >= Calendar.current.startOfDay(for: Date()) } ZStack { MapViewSwiftUI(onLongPress: { coord in waypointCoordinate = coord @@ -71,7 +72,7 @@ struct NodeDetail: View { editingWaypoint = wpId presentingWaypointForm = true } - }, positions: todaysPositions, waypoints: Array(waypoints), + }, positions: lastTenThousand, waypoints: Array(waypoints), mapViewType: mapType, userTrackingMode: MKUserTrackingMode.none, showNodeHistory: meshMapShowNodeHistory, diff --git a/Meshtastic/Views/Nodes/NodeMap.swift b/Meshtastic/Views/Nodes/NodeMap.swift index 4cba5089..650a76a5 100644 --- a/Meshtastic/Views/Nodes/NodeMap.swift +++ b/Meshtastic/Views/Nodes/NodeMap.swift @@ -77,7 +77,7 @@ struct NodeMap: View { waypoints: Array(waypoints), mapViewType: mapType, userTrackingMode: userTrackingMode, - showNodeHistory: meshMapShowNodeHistory, + showNodeHistory: meshMapShowNodeHistory, showRouteLines: meshMapShowRouteLines, customMapOverlay: self.customMapOverlay ) diff --git a/Meshtastic/Views/Settings/Config/DisplayConfig.swift b/Meshtastic/Views/Settings/Config/DisplayConfig.swift index 8a595a11..99d7e792 100644 --- a/Meshtastic/Views/Settings/Config/DisplayConfig.swift +++ b/Meshtastic/Views/Settings/Config/DisplayConfig.swift @@ -22,6 +22,7 @@ struct DisplayConfig: View { @State var screenCarouselInterval = 0 @State var gpsFormat = 0 @State var compassNorthTop = false + @State var wakeOnTapOrMotion = false @State var flipScreen = false @State var oledType = 0 @State var displayMode = 0 @@ -72,7 +73,14 @@ struct DisplayConfig: View { .toggleStyle(SwitchToggleStyle(tint: .accentColor)) Text("The compass heading on the screen outside of the circle will always point north.") .font(.caption) - + + Toggle(isOn: $wakeOnTapOrMotion) { + Label("Wake Screen on tap or motion", systemImage: "gyroscope") + } + .toggleStyle(SwitchToggleStyle(tint: .accentColor)) + Text("Requires that there be an accelerometer on your device.") + .font(.caption) + Toggle(isOn: $flipScreen) { Label("Flip Screen", systemImage: "pip.swap") @@ -151,6 +159,7 @@ struct DisplayConfig: View { dc.screenOnSecs = UInt32(screenOnSeconds) dc.autoScreenCarouselSecs = UInt32(screenCarouselInterval) dc.compassNorthTop = compassNorthTop + dc.wakeOnTapOrMotion = wakeOnTapOrMotion dc.flipScreen = flipScreen dc.oled = OledTypes(rawValue: oledType)!.protoEnumValue() dc.displaymode = DisplayModes(rawValue: displayMode)!.protoEnumValue() @@ -202,6 +211,11 @@ struct DisplayConfig: View { if newCompassNorthTop != node!.displayConfig!.compassNorthTop { hasChanges = true } } } + .onChange(of: wakeOnTapOrMotion) { newWakeOnTapOrMotion in + if node != nil && node!.displayConfig != nil { + if newWakeOnTapOrMotion != node!.displayConfig!.wakeOnTapOrMotion { hasChanges = true } + } + } .onChange(of: gpsFormat) { newGpsFormat in if node != nil && node!.displayConfig != nil { if newGpsFormat != node!.displayConfig!.gpsFormat { hasChanges = true } @@ -229,6 +243,7 @@ struct DisplayConfig: View { self.screenOnSeconds = Int(node?.displayConfig?.screenOnSeconds ?? 0) self.screenCarouselInterval = Int(node?.displayConfig?.screenCarouselInterval ?? 0) self.compassNorthTop = node?.displayConfig?.compassNorthTop ?? false + self.wakeOnTapOrMotion = node?.displayConfig?.wakeOnTapOrMotion ?? false self.flipScreen = node?.displayConfig?.flipScreen ?? false self.oledType = Int(node?.displayConfig?.oledType ?? 0) self.displayMode = Int(node?.displayConfig?.displayMode ?? 0) diff --git a/Meshtastic/Views/Settings/Config/Module/MQTTConfig.swift b/Meshtastic/Views/Settings/Config/Module/MQTTConfig.swift index 33a57063..4cefda0c 100644 --- a/Meshtastic/Views/Settings/Config/Module/MQTTConfig.swift +++ b/Meshtastic/Views/Settings/Config/Module/MQTTConfig.swift @@ -79,8 +79,8 @@ struct MQTTConfig: View { .onChange(of: address, perform: { _ in let totalBytes = address.utf8.count // Only mess with the value if it is too big - if totalBytes > 30 { - let firstNBytes = Data(username.utf8.prefix(30)) + if totalBytes > 62 { + let firstNBytes = Data(username.utf8.prefix(62)) if let maxBytesString = String(data: firstNBytes, encoding: String.Encoding.utf8) { // Set the shortName back to the last place where it was the right size address = maxBytesString diff --git a/Meshtastic/Views/Settings/Config/Module/RtttlConfig.swift b/Meshtastic/Views/Settings/Config/Module/RtttlConfig.swift new file mode 100644 index 00000000..88c102a9 --- /dev/null +++ b/Meshtastic/Views/Settings/Config/Module/RtttlConfig.swift @@ -0,0 +1,142 @@ +// +// RingtoneConfig.swift +// Meshtastic +// +// Created by Garth Vander Houwen on 3/25/23. +// + +import SwiftUI + +struct RtttlConfig: View { + + @Environment(\.managedObjectContext) var context + @EnvironmentObject var bleManager: BLEManager + @Environment(\.dismiss) private var goBack + + var node: NodeInfoEntity? + + @State private var isPresentingSaveConfirm: Bool = false + @State var hasChanges = false + @State var ringtone: String = "" + + var body: some View { + VStack { + Form { + if node != nil && node?.metadata == nil && node?.num ?? 0 != bleManager.connectedPeripheral?.num ?? 0 { + Text("There has been no response to a request for device metadata over the admin channel for this node.") + .font(.callout) + .foregroundColor(.orange) + + } else if node != nil && node?.num ?? 0 != bleManager.connectedPeripheral?.num ?? 0 { + // Let users know what is going on if they are using remote admin and don't have the config yet + if node?.rtttlConfig == nil { + Text("RTTTL Ringtone config data was requested over the admin channel but no response has been returned from the remote node. You can check the status of admin message requests in the admin message log.") + .font(.callout) + .foregroundColor(.orange) + } else { + Text("Remote administration for: \(node?.user?.longName ?? "Unknown")") + .font(.title3) + .onAppear { + setRtttLConfigValue() + } + } + } else if node != nil && node?.num ?? 0 == bleManager.connectedPeripheral?.num ?? 0 { + Text("Configuration for: \(node?.user?.longName ?? "Unknown")") + .font(.title3) + } else { + Text("Please connect to a radio to configure settings.") + .font(.callout) + .foregroundColor(.orange) + } + Section(header: Text("options")) { + + HStack { + Label("ringtone", systemImage: "music.quarternote.3") + TextField("Ringtone Transfer Language", text: $ringtone, axis: .vertical) + .foregroundColor(.gray) + .autocapitalization(.none) + .disableAutocorrection(true) + .onChange(of: ringtone, perform: { _ in + + let totalBytes = ringtone.utf8.count + // Only mess with the value if it is too big + if totalBytes > 228 { + + let firstNBytes = Data(ringtone.utf8.prefix(228)) + if let maxBytesString = String(data: firstNBytes, encoding: String.Encoding.utf8) { + // Set the ringtone back to the last place where it was the right size + ringtone = maxBytesString + } + } + }) + .foregroundColor(.gray) + } + .keyboardType(.default) + Text("Ringtone Transfer Language(RTTTL) Ringtone String used by supported buzzers in external notifications.") + .font(.caption) + } + } + .disabled(self.bleManager.connectedPeripheral == nil || node?.rtttlConfig == nil) + Button { + isPresentingSaveConfirm = true + } label: { + Label("save", systemImage: "square.and.arrow.down") + } + .disabled(bleManager.connectedPeripheral == nil || !hasChanges) + .buttonStyle(.bordered) + .buttonBorderShape(.capsule) + .controlSize(.large) + .padding() + .confirmationDialog( + "are.you.sure", + isPresented: $isPresentingSaveConfirm, + titleVisibility: .visible + ) { + let nodeName = node?.user?.longName ?? NSLocalizedString("unknown", comment: "Unknown") + let buttonText = String.localizedStringWithFormat(NSLocalizedString("save.config %@", comment: "Save Config for %@"), nodeName) + Button(buttonText) { + + let connectedNode = getNodeInfo(id: bleManager.connectedPeripheral.num, context: context) + if connectedNode != nil { + let adminMessageId = bleManager.saveRtttlConfig(ringtone: ringtone, fromUser: connectedNode!.user!, toUser: node!.user!, adminIndex: connectedNode?.myInfo?.adminIndex ?? 0) + if adminMessageId > 0 { + // Should show a saved successfully alert once I know that to be true + // for now just disable the button after a successful save + hasChanges = false + goBack() + } + } + } + } + message: { + Text("config.save.confirm") + } + .navigationTitle("ringtone.config") + .navigationBarItems(trailing: + ZStack { + ConnectedDevice(bluetoothOn: bleManager.isSwitchedOn, deviceConnected: bleManager.connectedPeripheral != nil, name: (bleManager.connectedPeripheral != nil) ? bleManager.connectedPeripheral.shortName : "????") + }) + .onAppear { + self.bleManager.context = context + setRtttLConfigValue() + // Need to request a Rtttl Config from the remote node before allowing changes + if bleManager.connectedPeripheral != nil && (node?.rtttlConfig == nil || node?.rtttlConfig?.ringtone?.count ?? 0 == 0) { + let connectedNode = getNodeInfo(id: bleManager.connectedPeripheral.num, context: context) + if node != nil && connectedNode != nil { + _ = bleManager.requestRtttlConfig(fromUser: connectedNode!.user!, toUser: node!.user!, adminIndex: connectedNode?.myInfo?.adminIndex ?? 0) + } + } + } + .onChange(of: ringtone) { newRingtone in + if node != nil && node!.rtttlConfig != nil { + if newRingtone != node!.rtttlConfig!.ringtone { hasChanges = true } + } + } + } + } + + func setRtttLConfigValue() { + self.ringtone = node?.rtttlConfig?.ringtone ?? "" + self.hasChanges = false + } +} diff --git a/Meshtastic/Views/Settings/Config/PositionConfig.swift b/Meshtastic/Views/Settings/Config/PositionConfig.swift index 25a91a69..55d61401 100644 --- a/Meshtastic/Views/Settings/Config/PositionConfig.swift +++ b/Meshtastic/Views/Settings/Config/PositionConfig.swift @@ -36,10 +36,14 @@ struct PositionConfig: View { @State var smartPositionEnabled = true @State var deviceGpsEnabled = true + @State var rxGpio = 0 + @State var txGpio = 0 @State var fixedPosition = false @State var gpsUpdateInterval = 0 @State var gpsAttemptTime = 0 @State var positionBroadcastSeconds = 0 + @State var broadcastSmartMinimumDistance = 0 + @State var broadcastSmartMinimumIntervalSecs = 0 @State var positionFlags = 3 /// Position Flags @@ -98,54 +102,53 @@ struct PositionConfig: View { .font(.callout) .foregroundColor(.orange) } - Section(header: Text("Device GPS")) { - Toggle(isOn: $deviceGpsEnabled) { - Label("Device GPS Enabled", systemImage: "location") - } - .toggleStyle(SwitchToggleStyle(tint: .accentColor)) - if deviceGpsEnabled { - Picker("Update Interval", selection: $gpsUpdateInterval) { - ForEach(GpsUpdateIntervals.allCases) { ui in - Text(ui.description) - } - } - Text("How often should we try to get a GPS position.") - .font(.caption) - Picker("Attempt Time", selection: $gpsAttemptTime) { - ForEach(GpsAttemptTimes.allCases) { at in - Text(at.description) - } - } - .pickerStyle(DefaultPickerStyle()) - Text("How long should we try to get our position during each GPS Update Interval attempt?") - .font(.caption) - } else { - Toggle(isOn: $fixedPosition) { - Label("Fixed Position", systemImage: "location.square.fill") - } - .toggleStyle(SwitchToggleStyle(tint: .accentColor)) - Text("If enabled your current location will be set as a fixed position.") - .font(.caption) - } - } Section(header: Text("Position Packet")) { - Toggle(isOn: $smartPositionEnabled) { - - Label("Smart Position Broadcast", systemImage: "location.fill.viewfinder") - } - .toggleStyle(SwitchToggleStyle(tint: .accentColor)) - Picker("Position Broadcast Interval", selection: $positionBroadcastSeconds) { ForEach(UpdateIntervals.allCases) { at in Text(at.description) } } .pickerStyle(DefaultPickerStyle()) - - Text("We should send our position this often (but only if it has changed significantly)") + Text("The maximum interval that can elapse without a node sending a position") .font(.caption) + + Toggle(isOn: $smartPositionEnabled) { + + Label("Smart Position Broadcast", systemImage: "location.fill.viewfinder") + } + .toggleStyle(SwitchToggleStyle(tint: .accentColor)) + + if smartPositionEnabled { + Picker("Minimum Broadcast Interval", selection: $broadcastSmartMinimumIntervalSecs) { + ForEach(UpdateIntervals.allCases) { at in + Text(at.description) + } + } + .pickerStyle(DefaultPickerStyle()) + Text("The fastest that position updates will be sent if the minimum distance has been satisfied") + .font(.caption) + + Picker("Minimum Distance", selection: $broadcastSmartMinimumDistance) { + ForEach(10..<151) { + + if $0 == 0 { + Text("unset") + } else { + + if $0.isMultiple(of: 5) { + Text("\($0)") + .tag($0) + } + + } + } + } + .pickerStyle(DefaultPickerStyle()) + Text("The minimum distance change in meters to be considered for a smart position broadcast.") + .font(.caption) + } } Section(header: Text("Position Flags")) { @@ -209,6 +212,59 @@ struct PositionConfig: View { .toggleStyle(SwitchToggleStyle(tint: .accentColor)) } } + Section(header: Text("Device GPS")) { + Toggle(isOn: $deviceGpsEnabled) { + Label("Device GPS Enabled", systemImage: "location") + } + .toggleStyle(SwitchToggleStyle(tint: .accentColor)) + + if deviceGpsEnabled { + Picker("Update Interval", selection: $gpsUpdateInterval) { + ForEach(GpsUpdateIntervals.allCases) { ui in + Text(ui.description) + } + } + Text("How often should we try to get a GPS position.") + .font(.caption) + Picker("Attempt Time", selection: $gpsAttemptTime) { + ForEach(GpsAttemptTimes.allCases) { at in + Text(at.description) + } + } + .pickerStyle(DefaultPickerStyle()) + Text("How long should we try to get our position during each GPS Update Interval attempt?") + .font(.caption) + + Picker("GPS Receive GPIO", selection: $rxGpio) { + ForEach(0..<40) { + if $0 == 0 { + Text("unset") + } else { + Text("Pin \($0)") + } + } + } + .pickerStyle(DefaultPickerStyle()) + + Picker("GPS Transmit GPIO", selection: $txGpio) { + ForEach(0..<40) { + if $0 == 0 { + Text("unset") + } else { + Text("Pin \($0)") + } + } + } + .pickerStyle(DefaultPickerStyle()) + } else { + Toggle(isOn: $fixedPosition) { + Label("Fixed Position", systemImage: "location.square.fill") + } + .toggleStyle(SwitchToggleStyle(tint: .accentColor)) + Text("If enabled your current location will be set as a fixed position.") + .font(.caption) + } + } } .disabled(self.bleManager.connectedPeripheral == nil || node?.positionConfig == nil) @@ -232,7 +288,7 @@ struct PositionConfig: View { Button(buttonText) { if fixedPosition { - _ = bleManager.sendPosition(destNum: node!.num, wantResponse: true) + _ = bleManager.sendPosition(destNum: node!.num, wantResponse: true, smartPosition: false) } let connectedNode = getNodeInfo(id: bleManager.connectedPeripheral.num, context: context) @@ -244,6 +300,8 @@ struct PositionConfig: View { pc.gpsUpdateInterval = UInt32(gpsUpdateInterval) pc.gpsAttemptTime = UInt32(gpsAttemptTime) pc.positionBroadcastSecs = UInt32(positionBroadcastSeconds) + pc.broadcastSmartMinimumIntervalSecs = UInt32(broadcastSmartMinimumIntervalSecs) + pc.broadcastSmartMinimumDistance = UInt32(broadcastSmartMinimumDistance) var pf: PositionFlags = [] if includeAltitude { pf.insert(.Altitude) } if includeAltitudeMsl { pf.insert(.AltitudeMsl) } @@ -296,6 +354,16 @@ struct PositionConfig: View { if newDeviceGps != node!.positionConfig!.deviceGpsEnabled { hasChanges = true } } } + .onChange(of: rxGpio) { newRxGpio in + if node != nil && node!.positionConfig != nil { + if newRxGpio != node!.positionConfig!.rxGpio { hasChanges = true } + } + } + .onChange(of: txGpio) { newTxGpio in + if node != nil && node!.positionConfig != nil { + if newTxGpio != node!.positionConfig!.txGpio { hasChanges = true } + } + } .onChange(of: gpsAttemptTime) { newGpsAttemptTime in if node != nil && node!.positionConfig != nil { if newGpsAttemptTime != node!.positionConfig!.gpsAttemptTime { hasChanges = true } @@ -321,6 +389,16 @@ struct PositionConfig: View { if newPositionBroadcastSeconds != node!.positionConfig!.positionBroadcastSeconds { hasChanges = true } } } + .onChange(of: broadcastSmartMinimumIntervalSecs) { newBroadcastSmartMinimumIntervalSecs in + if node != nil && node!.positionConfig != nil { + if newBroadcastSmartMinimumIntervalSecs != node!.positionConfig!.broadcastSmartMinimumIntervalSecs { hasChanges = true } + } + } + .onChange(of: broadcastSmartMinimumDistance) { newBroadcastSmartMinimumDistance in + if node != nil && node!.positionConfig != nil { + if newBroadcastSmartMinimumDistance != node!.positionConfig!.broadcastSmartMinimumDistance { hasChanges = true } + } + } .onChange(of: includeAltitude) { altFlag in let pf = PositionFlags(rawValue: self.positionFlags) let existingValue = pf.contains(.Altitude) @@ -382,10 +460,14 @@ struct PositionConfig: View { self.smartPositionEnabled = node?.positionConfig?.smartPositionEnabled ?? true self.deviceGpsEnabled = node?.positionConfig?.deviceGpsEnabled ?? true + self.rxGpio = Int(node?.positionConfig?.rxGpio ?? 0) + self.txGpio = Int(node?.positionConfig?.txGpio ?? 0) self.fixedPosition = node?.positionConfig?.fixedPosition ?? false self.gpsUpdateInterval = Int(node?.positionConfig?.gpsUpdateInterval ?? 30) self.gpsAttemptTime = Int(node?.positionConfig?.gpsAttemptTime ?? 30) self.positionBroadcastSeconds = Int(node?.positionConfig?.positionBroadcastSeconds ?? 900) + self.broadcastSmartMinimumIntervalSecs = Int(node?.positionConfig?.broadcastSmartMinimumIntervalSecs ?? 30) + self.broadcastSmartMinimumDistance = Int(node?.positionConfig?.broadcastSmartMinimumDistance ?? 50) self.positionFlags = Int(node?.positionConfig?.positionFlags ?? 3) let pf = PositionFlags(rawValue: self.positionFlags) diff --git a/Meshtastic/Views/Settings/Settings.swift b/Meshtastic/Views/Settings/Settings.swift index 6074fb0c..b5c8fbd6 100644 --- a/Meshtastic/Views/Settings/Settings.swift +++ b/Meshtastic/Views/Settings/Settings.swift @@ -35,6 +35,7 @@ struct Settings: View { case externalNotificationConfig case mqttConfig case rangeTestConfig + case ringtoneConfig case serialConfig case telemetryConfig case meshLog @@ -229,7 +230,14 @@ struct Settings: View { .symbolRenderingMode(.hierarchical) Text("range.test") } - .tag(SettingsSidebar.rangeTestConfig) + NavigationLink { + RtttlConfig(node: nodes.first(where: { $0.num == selectedNode })) + } label: { + Image(systemName: "music.note.list") + .symbolRenderingMode(.hierarchical) + Text("ringtone") + } + .tag(SettingsSidebar.ringtoneConfig) NavigationLink { SerialConfig(node: nodes.first(where: { $0.num == selectedNode })) diff --git a/Meshtastic/Views/Settings/ShareChannels.swift b/Meshtastic/Views/Settings/ShareChannels.swift index 5ca297dd..86d79877 100644 --- a/Meshtastic/Views/Settings/ShareChannels.swift +++ b/Meshtastic/Views/Settings/ShareChannels.swift @@ -224,7 +224,7 @@ struct ShareChannels: View { .font(.headline) .padding(.bottom) Text("Primary Channel").font(.title2) - Text("The first channel is the Primary channel and is where much of the mesh activity takes place. DM's are only available on the primary channel and it can not be disabled.") + Text("The first channel is the Primary channel and is where much of the mesh activity takes place. DM's are only available on the primary channel and it can not be disabled. If you don't share your primary channel, the first channel will become the primary channel on the other network and will allow communication with your mesh on the group channel.") .font(.callout) .padding([.leading, .trailing, .bottom]) Text("Admin Channel").font(.title2) diff --git a/Widgets/BatteryLevel.swift b/Widgets/BatteryLevel.swift new file mode 100644 index 00000000..c9bfdccd --- /dev/null +++ b/Widgets/BatteryLevel.swift @@ -0,0 +1,78 @@ +// +// BatteryLevel.swift +// Meshtastic +// +// Copyright Garth Vander Houwen 3/24/23. +// +import SwiftUI + +struct BatteryIcon: View { + var batteryLevel: Int32? + var font: Font + var color: Color + + var body: some View { + + if batteryLevel == 100 { + + Image(systemName: "battery.100.bolt") + .font(font) + .foregroundColor(color) + .symbolRenderingMode(.hierarchical) + } else if batteryLevel! < 100 && batteryLevel! > 74 { + + Image(systemName: "battery.75") + .font(font) + .foregroundColor(color) + .symbolRenderingMode(.hierarchical) + } else if batteryLevel! < 75 && batteryLevel! > 49 { + + Image(systemName: "battery.50") + .font(font) + .foregroundColor(color) + .symbolRenderingMode(.hierarchical) + } else if batteryLevel! < 50 && batteryLevel! > 14 { + + Image(systemName: "battery.25") + .font(font) + .foregroundColor(color) + .symbolRenderingMode(.hierarchical) + } else if batteryLevel! < 15 && batteryLevel! > 0 { + + Image(systemName: "battery.0") + .font(font) + .foregroundColor(color) + .symbolRenderingMode(.hierarchical) + + } else if batteryLevel! == 0 { + + Image(systemName: "battery.0") + .font(font) + .foregroundColor(.red) + .symbolRenderingMode(.hierarchical) + } else if batteryLevel! > 100 { + + Image(systemName: "powerplug") + .font(font) + .foregroundColor(color) + .symbolRenderingMode(.hierarchical) + } + } +} + +struct BatteryIcon_Previews: PreviewProvider { + static var previews: some View { + BatteryIcon(batteryLevel: 111, font: .title2, color: Color.accentColor) + .previewLayout(.fixed(width: 75, height: 75)) + BatteryIcon(batteryLevel: 100, font: .title2, color: Color.accentColor) + .previewLayout(.fixed(width: 75, height: 75)) + BatteryIcon(batteryLevel: 99, font: .title2, color: Color.accentColor) + .previewLayout(.fixed(width: 75, height: 75)) + BatteryIcon(batteryLevel: 74, font: .title2, color: Color.accentColor) + .previewLayout(.fixed(width: 75, height: 75)) + BatteryIcon(batteryLevel: 49, font: .title2, color: Color.accentColor) + .previewLayout(.fixed(width: 75, height: 75)) + BatteryIcon(batteryLevel: 14, font: .title2, color: Color.accentColor) + .previewLayout(.fixed(width: 75, height: 75)) + } +} diff --git a/Widgets/WidgetsLiveActivity.swift b/Widgets/WidgetsLiveActivity.swift index 835ecbfa..a9ffa68e 100644 --- a/Widgets/WidgetsLiveActivity.swift +++ b/Widgets/WidgetsLiveActivity.swift @@ -20,22 +20,60 @@ struct WidgetsLiveActivity: Widget { } dynamicIsland: { context in DynamicIsland { DynamicIslandExpandedRegion(.leading) { - NodeInfoView(nodeName: context.attributes.name, timerRange: context.state.timerRange, channelUtilization: context.state.channelUtilization, airtime: context.state.airtime, batteryLevel: context.state.batteryLevel) - .tint(Color("LightIndigo")) - .padding(.top) + Text("Network") + .font(.headline) + .fontWeight(.bold) + .foregroundStyle(.secondary) + .fixedSize() + .padding(.top, 10) + Text("\(String(format: "Ch. Util: %.2f", context.state.channelUtilization))%") + .font(.headline) + .fontWeight(.medium) + .foregroundStyle(.secondary) + .fixedSize() + Text("\(String(format: "Airtime: %.2f", context.state.airtime))%") + .font(.headline) + .fontWeight(.medium) + .foregroundStyle(.secondary) + .fixedSize() + Spacer() } - // Expanded UI goes here. Compose the expanded UI through - // various regions, like leading/trailing/center/bottom - DynamicIslandExpandedRegion(.trailing, priority: 1) { - HStack(alignment: .lastTextBaseline) { - - Spacer() - TimerView(timerRange: context.state.timerRange) - .tint(Color("LightIndigo")) + DynamicIslandExpandedRegion(.center) { + VStack(alignment: .center, spacing: 0) { + BatteryIcon(batteryLevel: Int32(context.state.batteryLevel), font: .title, color: .accentColor) + if context.state.batteryLevel == 0 { + Text("< 1%") + .font(.title3) + .foregroundColor(.gray) + .fixedSize() + } else if context.state.batteryLevel < 101 { + Text(String(context.state.batteryLevel) + "%") + .font(.title3) + .foregroundColor(.gray) + .fixedSize() + } else { + Text("Plugged In") + .font(.title3) + .foregroundColor(.gray) + } } - .padding(.top) + } + DynamicIslandExpandedRegion(.trailing, priority: 1) { + TimerView(timerRange: context.state.timerRange) + .tint(Color("LightIndigo")) } + DynamicIslandExpandedRegion(.bottom){ + Text(context.attributes.name) + .font(context.attributes.name.count > 14 ? .callout : .title3) + .fontWeight(.semibold) + .foregroundStyle(.tint) + Text("Last Heard: \(Date().formatted())") + .font(.caption) + .fontWeight(.medium) + .foregroundStyle(.secondary) + .fixedSize() + } } compactLeading: { Image("logo-black") @@ -65,7 +103,7 @@ struct WidgetsLiveActivity: Widget { @available(iOS 16.2, *) struct WidgetsLiveActivity_Previews: PreviewProvider { - static let attributes = MeshActivityAttributes(nodeNum: 123456789, name: "Meshtastic 8E6G") + static let attributes = MeshActivityAttributes(nodeNum: 123456789, name: "RAK Compact Rotary Handset Gray 8E6G") static let state = MeshActivityAttributes.ContentState( timerRange: Date.now...Date(timeIntervalSinceNow: 3600), connected: true, channelUtilization: 25.84, airtime: 10.01, batteryLevel: 39) @@ -108,7 +146,31 @@ struct LiveActivityView: View { Spacer() NodeInfoView(nodeName: nodeName, timerRange: timerRange, channelUtilization: channelUtilization, airtime: airtime, batteryLevel: batteryLevel) Spacer() - TimerView(timerRange: timerRange) + VStack { + BatteryIcon(batteryLevel: Int32(batteryLevel), font: .title, color: .secondary) + if batteryLevel == 0 { + Text("< 1%") + .font(.headline) + .fontWeight(.medium) + .foregroundStyle(.secondary) + .opacity(isLuminanceReduced ? 0.8 : 1.0) + .fixedSize() + } else if batteryLevel < 101 { + Text(String(batteryLevel) + "%") + .font(.headline) + .fontWeight(.medium) + .foregroundStyle(.secondary) + .opacity(isLuminanceReduced ? 0.8 : 1.0) + .fixedSize() + } else { + Text("Plugged In") + .font(.headline) + .fontWeight(.medium) + .foregroundStyle(.secondary) + .opacity(isLuminanceReduced ? 0.8 : 1.0) + .fixedSize() + } + } } .tint(.primary) .padding([.leading, .top, .bottom]) @@ -134,38 +196,47 @@ struct NodeInfoView: View { .fontWeight(.semibold) .foregroundStyle(.tint) Text("\(String(format: "Ch. Util: %.2f", channelUtilization))%") - .font(.subheadline) + .font(.headline) .fontWeight(.medium) .foregroundStyle(.secondary) .opacity(isLuminanceReduced ? 0.8 : 1.0) .fixedSize() Text("\(String(format: "Airtime: %.2f", airtime))%") - .font(.subheadline) + .font(.headline) .fontWeight(.medium) .foregroundStyle(.secondary) .opacity(isLuminanceReduced ? 0.8 : 1.0) .fixedSize() - if batteryLevel < 101 { - Text("Battery Level: \(batteryLevel > 0 ? String(batteryLevel) : "< 1")%") - .font(.subheadline) - .fontWeight(.medium) - .foregroundStyle(.secondary) - .opacity(isLuminanceReduced ? 0.8 : 1.0) - .fixedSize() - } else { - Text("Plugged In") - .font(.subheadline) - .fontWeight(.medium) - .foregroundStyle(.secondary) - .opacity(isLuminanceReduced ? 0.8 : 1.0) - .fixedSize() + let now = Date() + Text("Last Heard: \(now.formatted())") + .font(.caption) + .fontWeight(.medium) + .foregroundStyle(.secondary) + .opacity(isLuminanceReduced ? 0.8 : 1.0) + .fixedSize() + HStack { + + if timerRange.upperBound >= now { + Text("Next Update:") + .font(.caption) + .fontWeight(.medium) + .foregroundStyle(.secondary) + .opacity(isLuminanceReduced ? 0.8 : 1.0) + .fixedSize() + Text(timerInterval: timerRange, countsDown: true) + .monospacedDigit() + .multilineTextAlignment(.leading) + .font(.caption) + .fontWeight(.medium) + .foregroundStyle(.tint) + } else { + Text("Not Connected") + .multilineTextAlignment(.leading) + .font(.caption) + .fontWeight(.semibold) + .foregroundStyle(.tint) + } } - Text(Date().formatted()) - .font(.subheadline) - .fontWeight(.medium) - .foregroundStyle(.secondary) - .opacity(isLuminanceReduced ? 0.8 : 1.0) - .fixedSize() } } } @@ -177,18 +248,11 @@ struct TimerView: View { var body: some View { VStack(alignment: .center) { - Text("NEXT") + Text("NEXT UPDATE") .font(.caption) .fontWeight(.medium) .foregroundStyle(.secondary) .opacity(isLuminanceReduced ? 0.5 : 1.0) - .fixedSize() - Text("UPDATE") - .font(.caption) - .fontWeight(.medium) - .foregroundStyle(.secondary) - .opacity(isLuminanceReduced ? 0.5 : 1.0) - .fixedSize() Text(timerInterval: timerRange, countsDown: true) .monospacedDigit() .multilineTextAlignment(.center) @@ -215,7 +279,6 @@ struct ExpandedTrailingView: View { var body: some View { HStack(alignment: .lastTextBaseline) { - Spacer() TimerView(timerRange: timerInterval) } diff --git a/de.lproj/Localizable.strings b/de.lproj/Localizable.strings index c535db76..d42c9caa 100644 --- a/de.lproj/Localizable.strings +++ b/de.lproj/Localizable.strings @@ -144,6 +144,7 @@ "map.usertrackingmode.none"="None"; "map.usertrackingmode.follow"="Follow"; "map.usertrackingmode.followwithheading"="Follow with heading"; +"mesh.live.activity"="Mesh Live Activity"; "mesh.log"="Mesh Log"; "mesh.log.bluetooth.config %@"="Bluetooth Konfiguration empfangen: %@"; "mesh.log.cannedmessage.config %@"="Canned Message module config received: %@"; @@ -165,6 +166,7 @@ "mesh.log.position.config %@"="Positions Konfiguration empfangen: %@"; "mesh.log.position.received %@"="Positionspaket empfangen von Node: %@"; "mesh.log.rangetest.config %@"="Range Test Modul konfiguration empfangen: %@"; +"mesh.log.ringtone.config %@"="RTTTL Ringtone config received: %@"; "mesh.log.routing.message %@ %@"="Routing empfangen für RequestID: %@ Ack Status: %@"; "mesh.log.serial.config %@"="Serial Modul Konfiguration empfangen: %@"; "mesh.log.sharelocation %@"="Sent a Position Packet from the Apple device GPS to node: %@"; @@ -212,6 +214,8 @@ "reply"="Antworten"; "received.ack"="Empfangsbestätigung"; "received.ack.real"="Recipient Ack"; +"ringtone"="Ringtone"; +"ringtone.config"="Ringtone Config"; "routing.acknowledged"="Bestätigt"; "routing.noroute"="Keine Route"; "routing.gotnak"="Negative Empfangsbestätigung empfangen"; diff --git a/en.lproj/Localizable.strings b/en.lproj/Localizable.strings index df67d0e9..8183f1cf 100644 --- a/en.lproj/Localizable.strings +++ b/en.lproj/Localizable.strings @@ -49,8 +49,8 @@ "clear.log"="Clear Log"; "close"="Close"; "config.save.confirm"="After config values save the node will reboot."; -"connected.radio"="Connected Radio"; "communicating"="Communicating with device. ."; +"connected.radio"="Connected Radio"; "connected"="Currently Connected"; "connecting"="Connecting . ."; "contacts"="Contacts"; @@ -144,6 +144,7 @@ "map.usertrackingmode.follow"="Follow"; "map.usertrackingmode.followwithheading"="Follow with heading"; "map.usertrackingmode.none"="None"; +"mesh.live.activity"="Mesh Live Activity"; "mesh.log"="Mesh Log"; "mesh.log.bluetooth.config %@"="Bluetooth config received: %@"; "mesh.log.cannedmessage.config %@"="Canned Message module config received: %@"; @@ -165,6 +166,7 @@ "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.ringtone.config %@"="RTTTL Ringtone config received: %@"; "mesh.log.routing.message %@ %@"="Routing received for RequestID: %@ Ack Status: %@"; "mesh.log.serial.config %@"="Serial module config received: %@"; "mesh.log.sharelocation %@"="Sent a Position Packet from the Apple device GPS to node: %@"; @@ -212,6 +214,8 @@ "reboot.node"="Reboot node?"; "received.ack"="Received Ack"; "received.ack.real"="Recipient Ack"; +"ringtone"="Ringtone"; +"ringtone.config"="Ringtone Config"; "routing.acknowledged"="Acknowledged"; "routing.noroute"="No Route"; "routing.gotnak"="Received a negative acknowledgment"; diff --git a/zh-Hans.lproj/Localizable.strings b/zh-Hans.lproj/Localizable.strings index e2f07a11..f0f54149 100644 --- a/zh-Hans.lproj/Localizable.strings +++ b/zh-Hans.lproj/Localizable.strings @@ -144,6 +144,7 @@ "map.usertrackingmode.none"="None"; "map.usertrackingmode.follow"="Follow"; "map.usertrackingmode.followwithheading"="Follow with heading"; +"mesh.live.activity"="Mesh Live Activity"; "mesh.log"="Mesh 日志"; "mesh.log.bluetooth.config %@"="Bluetooth config received: %@"; "mesh.log.cannedmessage.config %@"="Canned Message module config received: %@"; @@ -165,6 +166,7 @@ "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.ringtone.config %@"="RTTTL Ringtone config received: %@"; "mesh.log.routing.message %@ %@"="Routing received for RequestID: %@ Ack Status: %@"; "mesh.log.serial.config %@"="Serial module config received: %@"; "mesh.log.sharelocation %@"="Sent a Position Packet from the Apple device GPS to node: %@"; @@ -212,6 +214,8 @@ "reboot.node"="重启节点?"; "received.ack"="收到确认"; "received.ack.real"="收件人确认"; +"ringtone"="Ringtone"; +"ringtone.config"="Ringtone Config"; "routing.acknowledged"="确认"; "routing.noroute"="找不到目标"; "routing.gotnak"="收到否认";