Merge pull request #1271 from meshtastic/2.6.7

2.6.7 Working Changes
This commit is contained in:
Garth Vander Houwen 2025-06-17 17:29:13 -07:00 committed by GitHub
commit e1e1724311
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 243 additions and 147 deletions

View file

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

View file

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

View file

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

View file

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

View file

@ -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<String>, isValid: Binding<Bool>) {
// Local state to store the value of iSSecure, or optionally a binding
private var isSecureBinding: Binding<Bool>?
@State private var isSecureLocal: Bool = true
private var isSecure: Binding<Bool> {
// Use the binding if we have one, otherwise fallback to the local state variable
isSecureBinding ?? $isSecureLocal
}
init(_ title: String, text: Binding<String>, isValid: Binding<Bool>, isSecure: Binding<Bool>? = 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())
}

View file

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