diff --git a/Localizable.xcstrings b/Localizable.xcstrings index d019ab3e..9a6c28c1 100644 --- a/Localizable.xcstrings +++ b/Localizable.xcstrings @@ -1362,7 +1362,7 @@ "it" : { "stringUnit" : { "state" : "translated", - "value" : "🦕 Versione di fine vita 🦖 ☄️" + "value" : "🦕 Versione a fine vita 🦖 ☄️" } }, "sr" : { @@ -1769,7 +1769,7 @@ "it" : { "stringUnit" : { "state" : "translated", - "value" : "Circa" + "value" : "Informazioni" } }, "sr" : { @@ -1903,7 +1903,7 @@ "it" : { "stringUnit" : { "state" : "translated", - "value" : "Riconosciuto" + "value" : "Confermato" } }, "pl" : { @@ -1943,7 +1943,7 @@ "it" : { "stringUnit" : { "state" : "translated", - "value" : "Riconosciuto da un altro nodo" + "value" : "Confermato da un altro nodo" } }, "sr" : { @@ -2152,6 +2152,12 @@ }, "Add Meshtastic Node %@ as a contact" : { "localizations" : { + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Aggiungi nodo Meshtastic %@ ai contatti" + } + }, "zh-Hant-TW" : { "stringUnit" : { "state" : "translated", @@ -2571,7 +2577,7 @@ "it" : { "stringUnit" : { "state" : "translated", - "value" : "Ora d'aria" + "value" : "Tempo di trasmissione" } }, "pl" : { @@ -2611,7 +2617,7 @@ "it" : { "stringUnit" : { "state" : "translated", - "value" : "Allarme" + "value" : "Avviso" } }, "sr" : { @@ -2633,7 +2639,7 @@ "it" : { "stringUnit" : { "state" : "translated", - "value" : "Avvisare il buzzer GPIO quando si riceve un campanello" + "value" : "Attiva il cicalino GPIO alla ricezione di una campana" } }, "sr" : { @@ -2661,7 +2667,7 @@ "it" : { "stringUnit" : { "state" : "translated", - "value" : "Avvisare il cicalino GPIO quando si riceve un messaggio" + "value" : "Attiva il cicalino GPIO alla ricezione di un messaggio" } }, "sr" : { @@ -2683,7 +2689,7 @@ "it" : { "stringUnit" : { "state" : "translated", - "value" : "Avviso GPIO del motore a vibrazione quando si riceve una campana" + "value" : "Attiva la vibrazione GPIO alla ricezione di una campana" } }, "sr" : { @@ -2711,7 +2717,7 @@ "it" : { "stringUnit" : { "state" : "translated", - "value" : "Avviso GPIO del motore vibrante alla ricezione di un messaggio" + "value" : "Attiva la vibrazione GPIO alla ricezione di un messaggio" } }, "sr" : { @@ -2733,7 +2739,7 @@ "it" : { "stringUnit" : { "state" : "translated", - "value" : "Avviso di ricezione di un campanello" + "value" : "Avvisa alla ricezione di una campana" } }, "sr" : { @@ -2761,7 +2767,7 @@ "it" : { "stringUnit" : { "state" : "translated", - "value" : "Avviso di ricezione di un messaggio" + "value" : "Avvisa alla ricezione di un messaggio" } }, "sr" : { @@ -2817,7 +2823,7 @@ "it" : { "stringUnit" : { "state" : "translated", - "value" : "Consentire le richieste di posizione" + "value" : "Consenti le richieste di posizione" } }, "sr" : { @@ -3811,7 +3817,7 @@ "it" : { "stringUnit" : { "state" : "translated", - "value" : "Richiesta negativa" + "value" : "Richiesta non valida" } }, "pl" : { @@ -4649,7 +4655,7 @@ "it" : { "stringUnit" : { "state" : "translated", - "value" : "Trasmette i pacchetti di posizione GPS come priorità." + "value" : "Dà priorità alla trasmissione di pacchetti di posizione GPS." } }, "pl" : { @@ -4707,7 +4713,7 @@ "it" : { "stringUnit" : { "state" : "translated", - "value" : "Trasmette regolarmente la posizione come messaggio al canale predefinito per assistere il recupero del dispositivo." + "value" : "Trasmette regolarmente la posizione come messaggio al canale predefinito per aiutare il recupero del dispositivo." } }, "pl" : { @@ -4833,7 +4839,7 @@ "it" : { "stringUnit" : { "state" : "translated", - "value" : "Acquistare radio complete" + "value" : "Acquista dispositivi completi" } }, "sr" : { @@ -4861,7 +4867,7 @@ "it" : { "stringUnit" : { "state" : "translated", - "value" : "Buzzer GPIO" + "value" : "Cicalino GPIO" } }, "sr" : { @@ -6224,7 +6230,7 @@ "it" : { "stringUnit" : { "state" : "translated", - "value" : "Libero" + "value" : "Svuota" } }, "sr" : { @@ -6960,7 +6966,7 @@ "it" : { "stringUnit" : { "state" : "translated", - "value" : "Radio connessa" + "value" : "Dispositivo connesso" } }, "zh-Hant-TW" : { @@ -7034,7 +7040,7 @@ "it" : { "stringUnit" : { "state" : "translated", - "value" : "La connessione a una nuova radio cancellerà tutti i dati delle app sul telefono." + "value" : "La connessione a un nuovo dispositivo cancellerà tutti i dati dell'applicazione sul telefono." } }, "zh-Hant-TW" : { @@ -7248,7 +7254,7 @@ "it" : { "stringUnit" : { "state" : "translated", - "value" : "Controlla il LED lampeggiante del dispositivo. Per la maggior parte dei dispositivi controlla uno dei 4 LED, mentre i LED del caricatore e del GPS non sono controllabili." + "value" : "Controlla il LED lampeggiante del dispositivo. Per la maggior parte dei dispositivi controlla uno dei 4 LED, mentre quelli di alimentazione e del GPS non sono controllabili." } }, "sr" : { @@ -7282,7 +7288,7 @@ "it" : { "stringUnit" : { "state" : "translated", - "value" : "Scafo convesso" + "value" : "Inviluppo convesso" } }, "sr" : { @@ -7310,7 +7316,7 @@ "it" : { "stringUnit" : { "state" : "translated", - "value" : "Coordinare" + "value" : "Coordinate" } }, "sr" : { @@ -7660,7 +7666,7 @@ "it" : { "stringUnit" : { "state" : "translated", - "value" : "Corrente: %lld" + "value" : "Attuale: %lld" } }, "sr" : { @@ -7682,7 +7688,7 @@ "it" : { "stringUnit" : { "state" : "translated", - "value" : "Attualmente mostra i moduli che potrebbero non essere supportati da questo nodo." + "value" : "Mostra i moduli che potrebbero non essere supportati al momento da questo nodo." } }, "zh-Hant-TW" : { @@ -8088,10 +8094,24 @@ } }, "Delete all config, keys and BLE bonds? " : { - + "localizations" : { + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Cancellare tutte le configurazioni, le chiavi e le associazioni bluetooth?" + } + } + } }, "Delete all config? " : { - + "localizations" : { + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Cancellare tutte le configurazioni?" + } + } + } }, "Delete all device metrics?" : { "localizations" : { @@ -10664,6 +10684,12 @@ }, "Enables the store and forward module." : { "localizations" : { + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Abilita il modulo Salva & Inoltra." + } + }, "zh-Hant-TW" : { "stringUnit" : { "state" : "translated", @@ -10673,10 +10699,24 @@ } }, "Enabling Ethernet will disable the bluetooth connection to the app. TCP node connections are not available on Apple devices." : { - + "localizations" : { + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Abilitando l'Ethernet verrà disabilita la connessione bluetooth all'applicazione. La connessione a nodi TCP non è disponibile su dispositivi Apple." + } + } + } }, "Enabling WiFi will disable the bluetooth connection to the app. TCP node connections are not available on Apple devices." : { - + "localizations" : { + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "L'attivazione del WiFi disabilita la connessione bluetooth all'applicazione. La connessione a nodi TCP non è disponibile su dispositivi Apple." + } + } + } }, "Encoder Press Event" : { "localizations" : { @@ -11580,7 +11620,14 @@ } }, "Factory reset will delete device and app data." : { - + "localizations" : { + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Il ripristino alle impostazioni di fabbrica eliminerà i dati del dispositivo e della app." + } + } + } }, "Failed to encode message content" : { "localizations" : { @@ -11659,7 +11706,7 @@ "it" : { "stringUnit" : { "state" : "translated", - "value" : "Fiera" + "value" : "Discreto" } }, "sr" : { @@ -12471,7 +12518,7 @@ "it" : { "stringUnit" : { "state" : "translated", - "value" : "Seguire" + "value" : "Segui" } }, "pl" : { @@ -12529,7 +12576,7 @@ "it" : { "stringUnit" : { "state" : "translated", - "value" : "Seguire con l'intestazione" + "value" : "Seguire la direzione" } }, "pl" : { @@ -12957,7 +13004,7 @@ "it" : { "stringUnit" : { "state" : "translated", - "value" : "Nome amichevole" + "value" : "Nome semplificato" } }, "sr" : { @@ -13642,7 +13689,7 @@ "it" : { "stringUnit" : { "state" : "translated", - "value" : "Intestazione" + "value" : "Direzione" } }, "sr" : { @@ -13664,7 +13711,7 @@ "it" : { "stringUnit" : { "state" : "translated", - "value" : "Intestazione: %@" + "value" : "Direzione: %@" } }, "sr" : { @@ -13967,7 +14014,7 @@ "it" : { "stringUnit" : { "state" : "translated", - "value" : "Via il luppolo" + "value" : "Distanza in Hop" } }, "sr" : { @@ -17907,7 +17954,7 @@ "it" : { "stringUnit" : { "state" : "translated", - "value" : "La maggior parte dei dati sulla rete viene inviata attraverso il canale primario. È possibile impostare canali secondari per creare gruppi di messaggistica aggiuntivi protetti da una propria chiave. [Suggerimenti per la configurazione del canale](https://meshtastic.org/docs/configuration/tips/)" + "value" : "La maggior parte dei dati sulla rete viene inviata attraverso il canale principale. È possibile impostare canali secondari per creare gruppi di messaggistica aggiuntivi protetti da una propria chiave. [Suggerimenti per la configurazione del canale](https://meshtastic.org/docs/configuration/tips/)" } }, "pl" : { @@ -21101,7 +21148,7 @@ "it" : { "stringUnit" : { "state" : "translated", - "value" : "Modalità di accoppiamento" + "value" : "Modalità di associazione" } }, "pl" : { @@ -22069,7 +22116,7 @@ "it" : { "stringUnit" : { "state" : "translated", - "value" : "Diario di posizione" + "value" : "Registro di posizione" } }, "sr" : { @@ -22711,7 +22758,7 @@ "it" : { "stringUnit" : { "state" : "translated", - "value" : "Primario" + "value" : "Principale" } }, "pl" : { @@ -22785,7 +22832,7 @@ "it" : { "stringUnit" : { "state" : "translated", - "value" : "GPIO primario" + "value" : "GPIO principale" } }, "sr" : { @@ -23327,7 +23374,7 @@ "it" : { "stringUnit" : { "state" : "translated", - "value" : "Test della gamma" + "value" : "Test di portata" } }, "pl" : { @@ -24072,7 +24119,7 @@ "it" : { "stringUnit" : { "state" : "translated", - "value" : "Rimuovere" + "value" : "Elimina" } }, "sr" : { @@ -24122,7 +24169,7 @@ "it" : { "stringUnit" : { "state" : "translated", - "value" : "Rimuovere da ignorato" + "value" : "Elimina da ignorati" } }, "sr" : { @@ -24184,7 +24231,7 @@ "it" : { "stringUnit" : { "state" : "translated", - "value" : "Sostituire i canali" + "value" : "Sostituisci canali" } }, "sr" : { @@ -25508,7 +25555,7 @@ "it" : { "stringUnit" : { "state" : "translated", - "value" : "Sats" + "value" : "Sat" } }, "sr" : { @@ -25536,7 +25583,7 @@ "it" : { "stringUnit" : { "state" : "translated", - "value" : "Stima Sats %lld" + "value" : "Stima satelliti %lld" } }, "sr" : { @@ -25564,7 +25611,7 @@ "it" : { "stringUnit" : { "state" : "translated", - "value" : "Saturazione in vista: %@" + "value" : "Satelliti in vista: %@" } }, "sr" : { @@ -25604,7 +25651,7 @@ "it" : { "stringUnit" : { "state" : "translated", - "value" : "Risparmiare" + "value" : "Salva" } }, "pl" : { @@ -26492,7 +26539,7 @@ "it" : { "stringUnit" : { "state" : "translated", - "value" : "Invia una posizione sul canale primario quando si fa triplo clic sul pulsante utente." + "value" : "Invia una posizione sul canale principale quando si fa triplo clic sul pulsante utente." } }, "sr" : { @@ -26576,7 +26623,7 @@ "it" : { "stringUnit" : { "state" : "translated", - "value" : "Invia una campana ASCII con un messaggio di avviso. Utile per attivare una notifica esterna sul campanello." + "value" : "Invia una campana ASCII con un messaggio di avviso. Utile per attivare notifiche esterne alla ricezione della campana." } }, "sr" : { @@ -28963,6 +29010,12 @@ "value" : "סטנדרטי" } }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Predefinito" + } + }, "pl" : { "stringUnit" : { "state" : "translated", @@ -29006,7 +29059,7 @@ "it" : { "stringUnit" : { "state" : "translated", - "value" : "Standard Silenzioso" + "value" : "Predefinito Silenzioso" } }, "pl" : { @@ -29126,7 +29179,7 @@ "it" : { "stringUnit" : { "state" : "translated", - "value" : "Memorizzare e inoltrare" + "value" : "Salva & Inoltra" } }, "sr" : { @@ -29154,7 +29207,7 @@ "it" : { "stringUnit" : { "state" : "translated", - "value" : "Configurazione Store & Forward" + "value" : "Configurazione Salva & Inoltra" } }, "sr" : { @@ -33190,7 +33243,7 @@ "it" : { "stringUnit" : { "state" : "translated", - "value" : "Utilizzare un'uscita PWM (come il cicalino RAK) per le sintonie invece di un'uscita on/off. In questo modo si ignorano le impostazioni di uscita, durata e attivazione e si utilizza invece l'opzione GPIO del buzzer configurata dal dispositivo." + "value" : "Utilizzare un'uscita PWM (come il cicalino RAK) per le sintonie invece di un'uscita on/off. In questo modo si ignorano le impostazioni di uscita, durata e attivazione e si utilizza invece l'opzione GPIO del cicalino configurata dal dispositivo." } }, "sr" : { @@ -35016,7 +35069,7 @@ "it" : { "stringUnit" : { "state" : "translated", - "value" : "La frequenza operativa del nodo viene calcolata in base alla regione, alla preimpostazione del modem e a questo campo. Se il campo è 0, lo slot viene calcolato automaticamente in base al nome del canale primario." + "value" : "La frequenza operativa del nodo viene calcolata in base alla regione, alla preimpostazione del modem e a questo campo. Se il campo è 0, lo slot viene calcolato automaticamente in base al nome del canale principale." } }, "sr" : { diff --git a/Meshtastic.xcodeproj/project.pbxproj b/Meshtastic.xcodeproj/project.pbxproj index 58b50820..6f72607e 100644 --- a/Meshtastic.xcodeproj/project.pbxproj +++ b/Meshtastic.xcodeproj/project.pbxproj @@ -1808,7 +1808,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 2.6.6; + MARKETING_VERSION = 2.6.7; PRODUCT_BUNDLE_IDENTIFIER = gvh.MeshtasticClient; PRODUCT_NAME = "$(TARGET_NAME)"; SUPPORTS_MACCATALYST = YES; @@ -1841,7 +1841,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 2.6.6; + MARKETING_VERSION = 2.6.7; PRODUCT_BUNDLE_IDENTIFIER = gvh.MeshtasticClient; PRODUCT_NAME = "$(TARGET_NAME)"; SUPPORTS_MACCATALYST = YES; @@ -1872,7 +1872,7 @@ "@executable_path/Frameworks", "@executable_path/../../Frameworks", ); - MARKETING_VERSION = 2.6.6; + MARKETING_VERSION = 2.6.7; PRODUCT_BUNDLE_IDENTIFIER = gvh.MeshtasticClient.Widgets; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; @@ -1904,7 +1904,7 @@ "@executable_path/Frameworks", "@executable_path/../../Frameworks", ); - MARKETING_VERSION = 2.6.6; + MARKETING_VERSION = 2.6.7; PRODUCT_BUNDLE_IDENTIFIER = gvh.MeshtasticClient.Widgets; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; diff --git a/Meshtastic/Helpers/BLEManager.swift b/Meshtastic/Helpers/BLEManager.swift index b19ea46d..84c22341 100644 --- a/Meshtastic/Helpers/BLEManager.swift +++ b/Meshtastic/Helpers/BLEManager.swift @@ -32,6 +32,7 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate public var isConnecting: Bool = false public var isConnected: Bool = false public var isSubscribed: Bool = false + public var allowDisconnect: Bool = false private var configNonce: UInt32 = 1 var timeoutTimer: Timer? var timeoutTimerCount = 0 @@ -59,8 +60,8 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate private var wantConfigTimer: Timer? private var wantConfigRetryCount = 0 - private let maxWantConfigRetries = 3 - private let wantConfigTimeoutInterval: TimeInterval = 6.0 + private let maxWantConfigRetries = 2 + private let wantConfigTimeoutInterval: TimeInterval = 5.0 // MARK: init private override init() { @@ -172,6 +173,7 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate isConnecting = false isConnected = false isSubscribed = false + allowDisconnect = false self.connectedPeripheral = nil invalidVersion = false connectedVersion = "0.0.0" @@ -193,12 +195,18 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate if self.mqttProxyConnected { self.mqttManager.mqttClientProxy?.disconnect() } - self.wantConfigTimer?.invalidate() + self.isWaitingForWantConfigResponse = false + if wantConfigTimer != nil { + self.wantConfigTimer?.invalidate() + } + self.wantConfigTimer = nil + self.wantConfigRetryCount = 0 self.automaticallyReconnect = reconnect self.centralManager?.cancelPeripheralConnection(connectedPeripheral.peripheral) self.FROMRADIO_characteristic = nil self.isConnected = false self.isSubscribed = false + self.allowDisconnect = false self.invalidVersion = false self.connectedVersion = "0.0.0" self.stopScanning() @@ -506,22 +514,22 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate } return success } - + func sendWantConfig() { isWaitingForWantConfigResponse = true - + guard connectedPeripheral?.peripheral.state ?? CBPeripheralState.disconnected == CBPeripheralState.connected else { return } - + if FROMRADIO_characteristic == nil { Logger.mesh.error("🚨 \("Unsupported Firmware Version Detected, unable to connect to device.".localized, privacy: .public)") invalidVersion = true return } else { - + let nodeName = connectedPeripheral?.peripheral.name ?? "Unknown".localized let logString = String.localizedStringWithFormat("Issuing Want Config to %@".localized, nodeName) Logger.mesh.info("🛎️ \(logString, privacy: .public)") - + // BLE Characteristics discovered, issue wantConfig var toRadio: ToRadio = ToRadio() configNonce = UInt32(NONCE_ONLY_DB) @@ -533,11 +541,9 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate return } connectedPeripheral!.peripheral.writeValue(binaryData, for: TORADIO_characteristic, type: .withResponse) - // Either Read the config complete value or from num notify value guard connectedPeripheral != nil else { return } connectedPeripheral!.peripheral.readValue(for: FROMRADIO_characteristic) - // Start timeout timer startWantConfigTimeout() } @@ -546,7 +552,6 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate private func startWantConfigTimeout() { // Cancel any existing timer wantConfigTimer?.invalidate() - // Start new timer wantConfigTimer = Timer.scheduledTimer(withTimeInterval: wantConfigTimeoutInterval, repeats: false) { [weak self] _ in self?.handleWantConfigTimeout() @@ -555,15 +560,13 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate private func handleWantConfigTimeout() { guard isWaitingForWantConfigResponse else { return } - wantConfigRetryCount += 1 - if wantConfigRetryCount < maxWantConfigRetries { Logger.mesh.warning("⏰ Want Config timeout, retrying... (attempt \(self.wantConfigRetryCount + 1)/\(self.maxWantConfigRetries))") sendWantConfig() } else { Logger.mesh.error("🚨 Want Config failed after \(self.maxWantConfigRetries) attempts, forcing disconnect") - forceDisconnect() + lastConnectionError = "Bluetooth connection timeout, keep your node closer or reboot your radio if the problem continues.".localized } } @@ -576,19 +579,6 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate } } - private func forceDisconnect() { - isWaitingForWantConfigResponse = false - wantConfigTimer?.invalidate() - wantConfigTimer = nil - wantConfigRetryCount = 0 - - disconnectPeripheral(reconnect: false) - - lastConnectionError = "Bluetooth connection timeout, keep your node closer.".localized - - Logger.mesh.error("💥 [BLE] Forced disconnect due to Want Config timeout") - } - // Call this to reset the retry mechanism (e.g., on new connection) func resetWantConfigRetries() { wantConfigRetryCount = 0 @@ -1066,6 +1056,7 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate invalidVersion = false lastConnectionError = "" isSubscribed = true + allowDisconnect = true Logger.mesh.info("🤜 [BLE] Want Config Complete. ID:\(decodedInfo.configCompleteID, privacy: .public)") if sendTime() { } diff --git a/Meshtastic/Views/Bluetooth/Connect.swift b/Meshtastic/Views/Bluetooth/Connect.swift index 4e114bb9..1176241a 100644 --- a/Meshtastic/Views/Bluetooth/Connect.swift +++ b/Meshtastic/Views/Bluetooth/Connect.swift @@ -90,40 +90,7 @@ struct Connect: View { .foregroundColor(Color.gray) .padding([.top]) .swipeActions { - Button(role: .destructive) { - if let connectedPeripheral = bleManager.connectedPeripheral, - connectedPeripheral.peripheral.state == .connected { - bleManager.disconnectPeripheral(reconnect: false) - } - } label: { - Label("Disconnect", systemImage: "antenna.radiowaves.left.and.right.slash") - } - } - .contextMenu { - - if node != nil { - #if !targetEnvironment(macCatalyst) - Button { - if !liveActivityStarted { - #if canImport(ActivityKit) - Logger.services.info("Start live activity.") - startNodeActivity() - #endif - } else { - #if canImport(ActivityKit) - Logger.services.info("Stop live activity.") - endActivity() - #endif - } - } label: { - Label("Mesh Live Activity", systemImage: liveActivityStarted ? "stop" : "play") - } - #endif - Text("Num: \(String(node!.num))") - Text("Short Name: \(node?.user?.shortName ?? "?")") - Text("Long Name: \(node?.user?.longName?.addingVariationSelectors ?? "Unknown".localized)") - Text("BLE RSSI: \(connectedPeripheral.rssi)") - + if bleManager.allowDisconnect { Button(role: .destructive) { if let connectedPeripheral = bleManager.connectedPeripheral, connectedPeripheral.peripheral.state == .connected { @@ -132,13 +99,51 @@ struct Connect: View { } label: { Label("Disconnect", systemImage: "antenna.radiowaves.left.and.right.slash") } - Button { - if !bleManager.sendShutdown(fromUser: node!.user!, toUser: node!.user!) { - Logger.mesh.error("Shutdown Failed") - } + } + } + .contextMenu { - } label: { - Label("Power Off", systemImage: "power") + if node != nil { + #if !targetEnvironment(macCatalyst) + if bleManager.isSubscribed { + Button { + if !liveActivityStarted { + #if canImport(ActivityKit) + Logger.services.info("Start live activity.") + startNodeActivity() + #endif + } else { + #if canImport(ActivityKit) + Logger.services.info("Stop live activity.") + endActivity() + #endif + } + } label: { + Label("Mesh Live Activity", systemImage: liveActivityStarted ? "stop" : "play") + } + } + #endif + Text("Num: \(String(node!.num))") + Text("Short Name: \(node?.user?.shortName ?? "?")") + Text("Long Name: \(node?.user?.longName?.addingVariationSelectors ?? "Unknown".localized)") + Text("BLE RSSI: \(connectedPeripheral.rssi)") + if bleManager.allowDisconnect { + Button(role: .destructive) { + if let connectedPeripheral = bleManager.connectedPeripheral, + connectedPeripheral.peripheral.state == .connected { + bleManager.disconnectPeripheral(reconnect: false) + } + } label: { + Label("Disconnect", systemImage: "antenna.radiowaves.left.and.right.slash") + } + Button { + if !bleManager.sendShutdown(fromUser: node!.user!, toUser: node!.user!) { + Logger.mesh.error("Shutdown Failed") + } + + } label: { + Label("Power Off", systemImage: "power") + } } } } @@ -154,7 +159,6 @@ struct Connect: View { } } } else { - if bleManager.isConnecting { HStack { Image(systemName: "antenna.radiowaves.left.and.right") diff --git a/Meshtastic/Views/Helpers/SecureInput.swift b/Meshtastic/Views/Helpers/SecureInput.swift index aaed8bd1..687cc6fe 100644 --- a/Meshtastic/Views/Helpers/SecureInput.swift +++ b/Meshtastic/Views/Helpers/SecureInput.swift @@ -12,19 +12,28 @@ struct SecureInput: View { private var idiom: UIUserInterfaceIdiom { UIDevice.current.userInterfaceIdiom } @Binding private var text: String @Binding private var isValid: Bool - @State var isSecure: Bool = true private var title: String - init(_ title: String, text: Binding, isValid: Binding) { + // Local state to store the value of iSSecure, or optionally a binding + private var isSecureBinding: Binding? + @State private var isSecureLocal: Bool = true + + private var isSecure: Binding { + // Use the binding if we have one, otherwise fallback to the local state variable + isSecureBinding ?? $isSecureLocal + } + + init(_ title: String, text: Binding, isValid: Binding, isSecure: Binding? = nil) { self.title = title self._text = text self._isValid = isValid + self.isSecureBinding = isSecure } var body: some View { ZStack(alignment: .trailing) { Group { - if isSecure { + if isSecure.wrappedValue { SecureField(title, text: $text) .font(idiom == .phone ? .caption : .callout) .allowsTightening(true) @@ -51,9 +60,9 @@ struct SecureInput: View { if !text.isEmpty { Button(action: { - isSecure.toggle() + isSecure.wrappedValue.toggle() }) { - Image(systemName: self.isSecure ? "eye.slash" : "eye") + Image(systemName: self.isSecure.wrappedValue ? "eye.slash" : "eye") .accentColor(.secondary) }.buttonStyle(BorderlessButtonStyle()) } diff --git a/Meshtastic/Views/Settings/Config/SecurityConfig.swift b/Meshtastic/Views/Settings/Config/SecurityConfig.swift index 2095ae9b..5954f47d 100644 --- a/Meshtastic/Views/Settings/Config/SecurityConfig.swift +++ b/Meshtastic/Views/Settings/Config/SecurityConfig.swift @@ -10,6 +10,7 @@ import SwiftUI import CoreData import MeshtasticProtobufs import OSLog +import CryptoKit struct SecurityConfig: View { @@ -33,6 +34,16 @@ struct SecurityConfig: View { @State var isManaged = false @State var serialEnabled = false @State var debugLogApiEnabled = false + @State var privateKeyIsSecure = true + + private var isValidKeyPair: Bool { + guard let privateKeyBytes = Data(base64Encoded: privateKey), + let calculatedPublicKey = generatePublicKeyDisplay(from: privateKeyBytes), + let decodedPublicKey = Data(base64Encoded: publicKey) else { + return false + } + return calculatedPublicKey == decodedPublicKey + } var body: some View { VStack { @@ -51,12 +62,16 @@ struct SecurityConfig: View { .foregroundStyle(.tertiary) .disableAutocorrection(true) .textSelection(.enabled) + .background( + RoundedRectangle(cornerRadius: 10.0) + .stroke(isValidKeyPair ? Color.clear : Color.red, lineWidth: 2.0) + ) Text("Sent out to other nodes on the mesh to allow them to compute a shared secret key.") .foregroundStyle(.secondary) .font(idiom == .phone ? .caption : .callout) Divider() Label("Private Key", systemImage: "key.fill") - SecureInput("Private Key", text: $privateKey, isValid: $hasValidPrivateKey) + SecureInput("Private Key", text: $privateKey, isValid: $hasValidPrivateKey, isSecure: $privateKeyIsSecure) .background( RoundedRectangle(cornerRadius: 10.0) .stroke(hasValidPrivateKey ? Color.clear : Color.red, lineWidth: 2.0) @@ -70,6 +85,7 @@ struct SecurityConfig: View { Button { if let keyBytes = generatePrivateKey(count: 32) { privateKey = keyBytes.base64EncodedString() + self.privateKeyIsSecure = false } } label: { Image(systemName: "lock.rotation") @@ -156,6 +172,10 @@ struct SecurityConfig: View { let tempKey = Data(base64Encoded: privateKey) ?? Data() if tempKey.count == 32 { hasValidPrivateKey = true + if let privateKeyBytes = Data(base64Encoded: privateKey), privateKeyBytes.count == 32 { + // Valid private key -- generate the public key + publicKey = generatePublicKeyDisplay(from: privateKeyBytes)?.base64EncodedString() ?? "" + } } else { hasValidPrivateKey = false } @@ -231,15 +251,13 @@ struct SecurityConfig: View { } var config = Config.SecurityConfig() - config.publicKey = Data(base64Encoded: publicKey) ?? Data() config.privateKey = Data(base64Encoded: privateKey) ?? Data() config.adminKey = [Data(base64Encoded: adminKey) ?? Data(), Data(base64Encoded: adminKey2) ?? Data(), Data(base64Encoded: adminKey3) ?? Data()] config.isManaged = isManaged config.serialEnabled = serialEnabled config.debugLogApiEnabled = debugLogApiEnabled - let reboot = node?.securityConfig?.privateKey?.base64EncodedString() ?? "" != privateKey - + let keyUpdated = node?.securityConfig?.privateKey?.base64EncodedString() ?? "" != privateKey let adminMessageId = bleManager.saveSecurityConfig( config: config, fromUser: fromUser, @@ -248,15 +266,18 @@ struct SecurityConfig: View { 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 - if reboot { - if !bleManager.sendReboot( - fromUser: fromUser, - toUser: toUser - ) { - Logger.mesh.warning("Reboot Failed") + if keyUpdated { + node?.user?.publicKey = Data(base64Encoded: publicKey) ?? Data() + do { + try context.save() + Logger.data.info("💾 Saved UserEntity Public Key to Core Data for \(node?.num ?? 0, privacy: .public)") + } catch { + context.rollback() + let nsError = error as NSError + Logger.data.error("Error Updating Core Data UserEntity: \(nsError, privacy: .public)") } } + hasChanges = false goBack() } } @@ -287,7 +308,25 @@ struct SecurityConfig: View { return randomBytes } else { // Handle error, perhaps by logging or throwing an exception - print("Error generating random bytes: \(status)") + Logger.mesh.debug("Error generating random bytes: \(status)") + return nil + } + } + + // Generate a new public key for display purposes to show the user what will be changed after the new private key is saved to the device + func generatePublicKeyDisplay(from privateKeyData: Data) -> Data? { + guard privateKeyData.count == 32 else { + Logger.mesh.debug("Invalid private key length. Must be 32 bytes for Curve25519.") + return nil + } + + do { + // Create a Curve25519 private key from raw representation + let privateKey = try Curve25519.KeyAgreement.PrivateKey(rawRepresentation: privateKeyData) + let publicKey = privateKey.publicKey + return publicKey.rawRepresentation + } catch { + Logger.mesh.debug("Failed to create Curve25519 key: \(error)") return nil } }