diff --git a/Localizable.xcstrings b/Localizable.xcstrings index 08f7b2ae..307e9e69 100644 --- a/Localizable.xcstrings +++ b/Localizable.xcstrings @@ -8691,6 +8691,12 @@ } } } + }, + "Disconnect Node" : { + + }, + "Disconnect the currently connected node" : { + }, "Dismiss" : { "localizations" : { @@ -23168,6 +23174,7 @@ } }, "Power Metrics Log}" : { + "extractionState" : "stale", "localizations" : { "sr" : { "stringUnit" : { diff --git a/Meshtastic.xcodeproj/project.pbxproj b/Meshtastic.xcodeproj/project.pbxproj index 58e2baa5..2d6ac2c6 100644 --- a/Meshtastic.xcodeproj/project.pbxproj +++ b/Meshtastic.xcodeproj/project.pbxproj @@ -62,6 +62,7 @@ BCB613832C672A2600485544 /* MessageChannelIntent.swift in Sources */ = {isa = PBXBuildFile; fileRef = BCB613822C672A2600485544 /* MessageChannelIntent.swift */; }; BCB613852C68703800485544 /* NodePositionIntent.swift in Sources */ = {isa = PBXBuildFile; fileRef = BCB613842C68703800485544 /* NodePositionIntent.swift */; }; BCB613872C69A0FB00485544 /* AppIntentErrors.swift in Sources */ = {isa = PBXBuildFile; fileRef = BCB613862C69A0FB00485544 /* AppIntentErrors.swift */; }; + BCD93CBA2D9E11A2006C9214 /* DisconnectNodeIntent.swift in Sources */ = {isa = PBXBuildFile; fileRef = BCD93CB92D9E11A2006C9214 /* DisconnectNodeIntent.swift */; }; BCE2D3C32C7ADF42008E6199 /* ShutDownNodeIntent.swift in Sources */ = {isa = PBXBuildFile; fileRef = BCE2D3C22C7ADF42008E6199 /* ShutDownNodeIntent.swift */; }; BCE2D3C52C7AE369008E6199 /* RestartNodeIntent.swift in Sources */ = {isa = PBXBuildFile; fileRef = BCE2D3C42C7AE369008E6199 /* RestartNodeIntent.swift */; }; BCE2D3C72C7B0D0A008E6199 /* ShortcutsProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = BCE2D3C62C7B0D0A008E6199 /* ShortcutsProvider.swift */; }; @@ -326,6 +327,7 @@ BCB613822C672A2600485544 /* MessageChannelIntent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageChannelIntent.swift; sourceTree = ""; }; BCB613842C68703800485544 /* NodePositionIntent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NodePositionIntent.swift; sourceTree = ""; }; BCB613862C69A0FB00485544 /* AppIntentErrors.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppIntentErrors.swift; sourceTree = ""; }; + BCD93CB92D9E11A2006C9214 /* DisconnectNodeIntent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DisconnectNodeIntent.swift; sourceTree = ""; }; BCE2D3C22C7ADF42008E6199 /* ShutDownNodeIntent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShutDownNodeIntent.swift; sourceTree = ""; }; BCE2D3C42C7AE369008E6199 /* RestartNodeIntent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RestartNodeIntent.swift; sourceTree = ""; }; BCE2D3C62C7B0D0A008E6199 /* ShortcutsProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShortcutsProvider.swift; sourceTree = ""; }; @@ -686,6 +688,7 @@ BCE2D3C62C7B0D0A008E6199 /* ShortcutsProvider.swift */, BC6B45FE2CB2F98900723CEB /* SaveChannelSettingsIntent.swift */, BC47C2EE2CE0017D008245CA /* MessageNodeIntent.swift */, + BCD93CB92D9E11A2006C9214 /* DisconnectNodeIntent.swift */, ); path = AppIntents; sourceTree = ""; @@ -1421,6 +1424,7 @@ DDC94FCE29CF55310082EA6E /* RtttlConfig.swift in Sources */, DD964FBD296E6B01007C176F /* EmojiOnlyTextField.swift in Sources */, DD8169FF272476C700F4AB02 /* LogDocument.swift in Sources */, + BCD93CBA2D9E11A2006C9214 /* DisconnectNodeIntent.swift in Sources */, DDC94FC129CE063B0082EA6E /* BatteryLevel.swift in Sources */, 231B3F252D087C3C0069A07D /* EnvironmentDefaultColumns.swift in Sources */, 25F5D5BE2C3F6D87008036E3 /* NavigationState.swift in Sources */, diff --git a/Meshtastic/AppIntents/DisconnectNodeIntent.swift b/Meshtastic/AppIntents/DisconnectNodeIntent.swift new file mode 100644 index 00000000..2e9986d0 --- /dev/null +++ b/Meshtastic/AppIntents/DisconnectNodeIntent.swift @@ -0,0 +1,31 @@ +// +// DisconnectNodeIntent.swift +// Meshtastic +// +// Created by Benjamin Faershtein on 4/2/25. +// + +import Foundation +import AppIntents + +struct DisconnectNodeIntent: AppIntent { + static var title: LocalizedStringResource = "Disconnect Node" + + static var description: IntentDescription = "Disconnect the currently connected node" + + + func perform() async throws -> some IntentResult { + if !BLEManager.shared.isConnected { + throw AppIntentErrors.AppIntentError.notConnected + } + + if let connectedPeripheral = BLEManager.shared.connectedPeripheral, + connectedPeripheral.peripheral.state == .connected { + BLEManager.shared.disconnectPeripheral(reconnect: false) + } else { + throw AppIntentErrors.AppIntentError.message("Error disconnecting node") + } + + return .result() + } +} diff --git a/Meshtastic/AppIntents/ShortcutsProvider.swift b/Meshtastic/AppIntents/ShortcutsProvider.swift index b21c7e7d..fcc9ffec 100644 --- a/Meshtastic/AppIntents/ShortcutsProvider.swift +++ b/Meshtastic/AppIntents/ShortcutsProvider.swift @@ -32,5 +32,13 @@ struct ShortcutsProvider: AppShortcutsProvider { "Send a \(.applicationName) group message"], shortTitle: "Group Message", systemImageName: "message") + + AppShortcut(intent: DisconnectNodeIntent(), + phrases: ["Disconnect \(.applicationName) node", + "Disconnect my \(.applicationName) node", + "Disconnect from \(.applicationName)", + "Disconnect \(.applicationName)"], + shortTitle: "Disconnect", + systemImageName: "antenna.radiowaves.left.and.right.slash") } } diff --git a/Meshtastic/Helpers/BLEManager.swift b/Meshtastic/Helpers/BLEManager.swift index 9f633f30..e4e9ae73 100644 --- a/Meshtastic/Helpers/BLEManager.swift +++ b/Meshtastic/Helpers/BLEManager.swift @@ -177,20 +177,25 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate // Disconnect Connected Peripheral func disconnectPeripheral(reconnect: Bool = true) { - - guard let connectedPeripheral = connectedPeripheral else { return } - if mqttProxyConnected { - mqttManager.mqttClientProxy?.disconnect() + // Ensure all operations run on the main thread + DispatchQueue.main.async { [weak self] in + guard let self = self else { return } + guard let connectedPeripheral = self.connectedPeripheral else { return } + + if self.mqttProxyConnected { + self.mqttManager.mqttClientProxy?.disconnect() + } + + self.automaticallyReconnect = reconnect + self.centralManager?.cancelPeripheralConnection(connectedPeripheral.peripheral) + self.FROMRADIO_characteristic = nil + self.isConnected = false + self.isSubscribed = false + self.invalidVersion = false + self.connectedVersion = "0.0.0" + self.stopScanning() + self.startScanning() } - automaticallyReconnect = reconnect - centralManager?.cancelPeripheralConnection(connectedPeripheral.peripheral) - FROMRADIO_characteristic = nil - isConnected = false - isSubscribed = false - invalidVersion = false - connectedVersion = "0.0.0" - stopScanning() - startScanning() } // Called each time a peripheral is connected