From 2ee000f84ac92db9024e0f64aad5c87f65b0bca6 Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Sat, 23 Oct 2021 10:27:10 -0700 Subject: [PATCH] V 1.26.10 Add file export functionality for Mesh Activity Log --- Meshtastic Client.xcodeproj/project.pbxproj | 8 ++++-- MeshtasticClient/Helpers/BLEManager.swift | 26 ++++++++++++++----- .../Views/Bluetooth/Connect.swift | 4 +-- .../Views/Messages/Messages.swift | 12 +++------ .../Views/Settings/LogDocument.swift | 25 ++++++++++++++++++ MeshtasticClient/Views/Settings/MeshLog.swift | 23 ++++++++++++++-- 6 files changed, 77 insertions(+), 21 deletions(-) create mode 100644 MeshtasticClient/Views/Settings/LogDocument.swift diff --git a/Meshtastic Client.xcodeproj/project.pbxproj b/Meshtastic Client.xcodeproj/project.pbxproj index 087297a9..2dfd51ca 100644 --- a/Meshtastic Client.xcodeproj/project.pbxproj +++ b/Meshtastic Client.xcodeproj/project.pbxproj @@ -22,6 +22,7 @@ DD4A91202708C66600501B7E /* Configuration.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD4A911F2708C66600501B7E /* Configuration.swift */; }; DD8169F9271F1A6100F4AB02 /* Logger.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD8169F8271F1A6100F4AB02 /* Logger.swift */; }; DD8169FB271F1F3A00F4AB02 /* MeshLog.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD8169FA271F1F3A00F4AB02 /* MeshLog.swift */; }; + DD8169FF272476C700F4AB02 /* LogDocument.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD8169FE272476C700F4AB02 /* LogDocument.swift */; }; DD836AE726F6B38600ABCC23 /* Connect.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD836AE626F6B38600ABCC23 /* Connect.swift */; }; DD836AED26F858F900ABCC23 /* MeshData.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD836AEC26F858F900ABCC23 /* MeshData.swift */; }; DD836AEF26F85D8D00ABCC23 /* NodeInfoModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD836AEE26F85D8D00ABCC23 /* NodeInfoModel.swift */; }; @@ -84,6 +85,7 @@ DD4A911F2708C66600501B7E /* Configuration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Configuration.swift; sourceTree = ""; }; DD8169F8271F1A6100F4AB02 /* Logger.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Logger.swift; sourceTree = ""; }; DD8169FA271F1F3A00F4AB02 /* MeshLog.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MeshLog.swift; sourceTree = ""; }; + DD8169FE272476C700F4AB02 /* LogDocument.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LogDocument.swift; sourceTree = ""; }; DD836AE626F6B38600ABCC23 /* Connect.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Connect.swift; sourceTree = ""; }; DD836AEC26F858F900ABCC23 /* MeshData.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MeshData.swift; sourceTree = ""; }; DD836AEE26F85D8D00ABCC23 /* NodeInfoModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NodeInfoModel.swift; sourceTree = ""; }; @@ -170,6 +172,7 @@ DD4A911D2708C65400501B7E /* AppSettings.swift */, DD4A911F2708C66600501B7E /* Configuration.swift */, DD8169FA271F1F3A00F4AB02 /* MeshLog.swift */, + DD8169FE272476C700F4AB02 /* LogDocument.swift */, ); path = Settings; sourceTree = ""; @@ -474,6 +477,7 @@ DDAF8C5326EB1DF10058C060 /* BLEManager.swift in Sources */, DD90860E26F69BAE00DC5189 /* NodeMap.swift in Sources */, DD47E3DB26F3901B00029299 /* Channels.swift in Sources */, + DD8169FF272476C700F4AB02 /* LogDocument.swift in Sources */, DDAF8C6926ED0D070058C060 /* deviceonly.pb.swift in Sources */, DD23A51326FEF5D500D9B90C /* MessageData.swift in Sources */, DD836AED26F858F900ABCC23 /* MeshData.swift in Sources */, @@ -668,7 +672,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 1.26.9; + MARKETING_VERSION = 1.26.10; PRODUCT_BUNDLE_IDENTIFIER = gvh.MeshtasticClient; PRODUCT_NAME = "$(TARGET_NAME)"; SUPPORTS_MACCATALYST = YES; @@ -695,7 +699,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 1.26.9; + MARKETING_VERSION = 1.26.10; PRODUCT_BUNDLE_IDENTIFIER = gvh.MeshtasticClient; PRODUCT_NAME = "$(TARGET_NAME)"; SUPPORTS_MACCATALYST = YES; diff --git a/MeshtasticClient/Helpers/BLEManager.swift b/MeshtasticClient/Helpers/BLEManager.swift index 07a0da2d..d6e29d8e 100644 --- a/MeshtasticClient/Helpers/BLEManager.swift +++ b/MeshtasticClient/Helpers/BLEManager.swift @@ -147,8 +147,11 @@ class BLEManager: NSObject, ObservableObject, CBCentralManagerDelegate, CBPeriph self.centralManager?.connect(peripheral) + // Use a timer to keep track of connecting peripherals, context to pass the radio name with the timer and the RunLoop to prevent + // the timer from running on the main UI thread let context = ["name": "@\(peripheral.name ?? "Unknown")"] self.timeoutTimer = Timer.scheduledTimer(timeInterval: 2.0, target: self, selector: #selector(timeoutTimerFired), userInfo: context, repeats: true) + RunLoop.current.add(self.timeoutTimer!, forMode: .common) } // Disconnect Device function @@ -186,28 +189,32 @@ class BLEManager: NSObject, ObservableObject, CBCentralManagerDelegate, CBPeriph // called when a peripheral is connected func centralManager(_ central: CBCentralManager, didConnect peripheral: CBPeripheral) { + peripheral.delegate = self + // Invalidate and reset connection timer count, remove any connection errors lastConnectionError = "" self.timeoutTimer!.invalidate() self.runCount = 0 - - peripheral.delegate = self + + // Map the peripheral to the connectedNode and connectedPeripheral ObservedObjects connectedPeripheral = peripherals.filter({ $0.peripheral.identifier == peripheral.identifier }).first let deviceName = peripheral.name ?? "" - let peripheralLast4: String = String(deviceName.suffix(4)) - connectedNode = meshData.nodes.first(where: { $0.user.id.contains(peripheralLast4) }) lastConnectedPeripheral = peripheral.identifier.uuidString + + // Discover Services peripheral.discoverServices([meshtasticServiceCBUUID]) if meshLoggingEnabled { Logger.log("BLE Connected: \(peripheral.name ?? "Unknown")") } print("BLE Connected: \(peripheral.name ?? "Unknown")") + // Clear the "Available Radios" list peripherals.removeAll() } // Disconnect Peripheral Event func centralManager(_ central: CBCentralManager, didDisconnectPeripheral peripheral: CBPeripheral, error: Error?) { + peripheral.delegate = self // Start a scan so the disconnected peripheral is moved to the peripherals[] if it is awake self.startScanning() self.connectedPeripheral = nil @@ -215,9 +222,11 @@ class BLEManager: NSObject, ObservableObject, CBCentralManagerDelegate, CBPeriph if let e = error { + // https://developer.apple.com/documentation/corebluetooth/cberror/code let errorCode = (e as NSError).code - - if errorCode == 6 { // The connection has timed out unexpectedly. + // unknown = 0, + + if errorCode == 6 { // CBError.Code.connectionTimeout The connection has timed out unexpectedly. // Happens when device is manually reset / powered off // We will try and re-connect to this device @@ -228,7 +237,7 @@ class BLEManager: NSObject, ObservableObject, CBCentralManagerDelegate, CBPeriph self.connectTo(peripheral: peripheral) } } - else if errorCode == 7 { // The specified device has disconnected from us. + 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. lastConnectionError = e.localizedDescription @@ -259,6 +268,7 @@ class BLEManager: NSObject, ObservableObject, CBCentralManagerDelegate, CBPeriph // Discover Services Event func peripheral(_ peripheral: CBPeripheral, didDiscoverServices error: Error?) { + peripheral.delegate = self if let e = error { print("Discover Services error \(e)") @@ -282,6 +292,7 @@ class BLEManager: NSObject, ObservableObject, CBCentralManagerDelegate, CBPeriph // Discover Characteristics Event func peripheral(_ peripheral: CBPeripheral, didDiscoverCharacteristicsFor service: CBService, error: Error?) { + peripheral.delegate = self if let e = error { print("Discover Characteristics error \(e)") @@ -328,6 +339,7 @@ class BLEManager: NSObject, ObservableObject, CBCentralManagerDelegate, CBPeriph // Data Read / Update Characteristic Event func peripheral(_ peripheral: CBPeripheral, didUpdateValueFor characteristic: CBCharacteristic, error: Error?) { + peripheral.delegate = self if let e = error { print("didUpdateValueFor Characteristic error \(e)") diff --git a/MeshtasticClient/Views/Bluetooth/Connect.swift b/MeshtasticClient/Views/Bluetooth/Connect.swift index 055e3ae3..17306731 100644 --- a/MeshtasticClient/Views/Bluetooth/Connect.swift +++ b/MeshtasticClient/Views/Bluetooth/Connect.swift @@ -38,7 +38,7 @@ struct Connect: View { .textCase(nil) } - Section(header: Text("Connected Device").font(.title)) { + Section(header: Text("Connected Radio").font(.title)) { if bleManager.connectedPeripheral != nil && bleManager.connectedPeripheral.peripheral.state == .connected { HStack { @@ -126,7 +126,7 @@ struct Connect: View { .textCase(nil) if bleManager.peripherals.count > 0 { - Section(header: Text("Available Devices").font(.title)) { + Section(header: Text("Available Radios").font(.title)) { ForEach(bleManager.peripherals.filter({ $0.peripheral.state == CBPeripheralState.disconnected }).sorted(by: { $0.rssi > $1.rssi })) { peripheral in HStack { Image(systemName: "circle.fill") diff --git a/MeshtasticClient/Views/Messages/Messages.swift b/MeshtasticClient/Views/Messages/Messages.swift index c7c6470b..ce45a231 100644 --- a/MeshtasticClient/Views/Messages/Messages.swift +++ b/MeshtasticClient/Views/Messages/Messages.swift @@ -154,6 +154,7 @@ struct Messages: View { .focused($focusedField, equals: .messageText) .multilineTextAlignment(.leading) .frame(minHeight: bounds.size.height / 4, maxHeight: bounds.size.height / 4) + Text(typingMessage).opacity(0).padding(.all, 0) @@ -187,14 +188,9 @@ struct Messages: View { .navigationBarTitleDisplayMode(.inline) .navigationBarItems(trailing: - ZStack { - - ConnectedDevice( - bluetoothOn: bleManager.isSwitchedOn, - deviceConnected: bleManager.connectedPeripheral != nil, - name: (bleManager.connectedNode != nil) ? bleManager.connectedNode.user.shortName : ((bleManager.connectedPeripheral != nil) ? bleManager.connectedPeripheral.name : "Unknown")) - } - ) + ZStack { + ConnectedDevice(bluetoothOn: bleManager.isSwitchedOn, deviceConnected: bleManager.connectedPeripheral != nil, name: (bleManager.connectedNode != nil) ? bleManager.connectedNode.user.shortName : ((bleManager.connectedPeripheral != nil) ? bleManager.connectedPeripheral.name : "Unknown") ) + }) .onAppear { messageData.load() diff --git a/MeshtasticClient/Views/Settings/LogDocument.swift b/MeshtasticClient/Views/Settings/LogDocument.swift new file mode 100644 index 00000000..5b65fd73 --- /dev/null +++ b/MeshtasticClient/Views/Settings/LogDocument.swift @@ -0,0 +1,25 @@ +import SwiftUI +import UniformTypeIdentifiers + +struct LogDocument: FileDocument{ + static var readableContentTypes:[UTType] {[.plainText]} + + var logFile: String + + init(logFile: String){ + self.logFile = logFile + } + + init(configuration: ReadConfiguration) throws{ + guard let data = configuration.file.regularFileContents, + let string = String(data: data, encoding: .utf8) + else{ + throw CocoaError(.fileReadCorruptFile) + } + logFile = string + } + + func fileWrapper(configuration: WriteConfiguration) throws -> FileWrapper { + return FileWrapper(regularFileWithContents: logFile.data(using: .utf8)!) + } +} diff --git a/MeshtasticClient/Views/Settings/MeshLog.swift b/MeshtasticClient/Views/Settings/MeshLog.swift index 77fccd1b..8fcf1cd5 100644 --- a/MeshtasticClient/Views/Settings/MeshLog.swift +++ b/MeshtasticClient/Views/Settings/MeshLog.swift @@ -1,10 +1,13 @@ import SwiftUI import Foundation +import UniformTypeIdentifiers struct MeshLog: View { let logFile = Logger.logFile var text = "" @State private var logs = [String]() + @State private var isExporting: Bool = false + @State private var document: LogDocument = LogDocument(logFile: "") var body: some View { @@ -16,12 +19,29 @@ struct MeshLog: View { logs.removeAll() for try await log in url.lines { logs.append(log) + document.logFile.append(log) } logs.reverse() } catch { // Stop adding logs when an error is thrown } } + .fileExporter(isPresented: $isExporting, + document: document, + contentType: UTType.plainText, + defaultFilename: "mesh-activity-log" + ) + { + result in + if case .success = result { + print("Upload was ok") + } + else{ + print("Something went wrong") + } + } + + .textSelection(.enabled) .font(.caption2) @@ -49,7 +69,7 @@ struct MeshLog: View { Spacer() Button(action: { - + isExporting = true }) { Image(systemName: "arrow.down.circle.fill").imageScale(.large).foregroundColor(.gray) Text("Download Log") @@ -59,7 +79,6 @@ struct MeshLog: View { .padding() .background(Color(.systemGray6)) .clipShape(Capsule()) - .hidden() Spacer()