diff --git a/Meshtastic Client.xcodeproj/project.pbxproj b/Meshtastic Client.xcodeproj/project.pbxproj index ddb9eb04..6a0a9262 100644 --- a/Meshtastic Client.xcodeproj/project.pbxproj +++ b/Meshtastic Client.xcodeproj/project.pbxproj @@ -20,6 +20,8 @@ DD47E3DF26F39D9F00029299 /* MyInfoModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD47E3DE26F39D9F00029299 /* MyInfoModel.swift */; }; DD4A911E2708C65400501B7E /* AppSettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD4A911D2708C65400501B7E /* AppSettings.swift */; }; 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 */; }; 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 */; }; @@ -80,6 +82,8 @@ DD47E3DE26F39D9F00029299 /* MyInfoModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MyInfoModel.swift; sourceTree = ""; }; DD4A911D2708C65400501B7E /* AppSettings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppSettings.swift; sourceTree = ""; }; 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 = ""; }; 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 = ""; }; @@ -165,6 +169,7 @@ children = ( DD4A911D2708C65400501B7E /* AppSettings.swift */, DD4A911F2708C66600501B7E /* Configuration.swift */, + DD8169FA271F1F3A00F4AB02 /* MeshLog.swift */, ); path = Settings; sourceTree = ""; @@ -318,6 +323,7 @@ DDAF8C6D26ED19040058C060 /* Extensions.swift */, DD47E3D126F1210600029299 /* HelperFunctions.swift */, DD913638270DFF4C00D7ACF3 /* LocalNotificationManager.swift */, + DD8169F8271F1A6100F4AB02 /* Logger.swift */, ); path = Helpers; sourceTree = ""; @@ -487,7 +493,9 @@ DDC2E18F26CE25FE0042C5E4 /* ContentView.swift in Sources */, DDAF8C6326ED0A230058C060 /* admin.pb.swift in Sources */, DDAF8C5826ED07FD0058C060 /* mesh.pb.swift in Sources */, + DD8169FB271F1F3A00F4AB02 /* MeshLog.swift in Sources */, DD47E3D926F3093800029299 /* MessageBubble.swift in Sources */, + DD8169F9271F1A6100F4AB02 /* Logger.swift in Sources */, DDAF8C6726ED0C8C0058C060 /* remote_hardware.pb.swift in Sources */, DDAF8C6526ED0A490058C060 /* channel.pb.swift in Sources */, DD47E3DD26F390A000029299 /* Messages.swift in Sources */, @@ -660,7 +668,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 1.26.2; + MARKETING_VERSION = 1.26.4; PRODUCT_BUNDLE_IDENTIFIER = gvh.MeshtasticClient; PRODUCT_NAME = "$(TARGET_NAME)"; SUPPORTS_MACCATALYST = YES; @@ -687,7 +695,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 1.26.2; + MARKETING_VERSION = 1.26.4; 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 1542d981..48b290f2 100644 --- a/MeshtasticClient/Helpers/BLEManager.swift +++ b/MeshtasticClient/Helpers/BLEManager.swift @@ -7,6 +7,17 @@ import SwiftUI // Meshtastic BLE Device Manager //--------------------------------------------------------------------------------------- class BLEManager: NSObject, ObservableObject, CBCentralManagerDelegate, CBPeripheralDelegate { + + private static var documentsFolder: URL { + do { + return try FileManager.default.url(for: .documentDirectory, + in: .userDomainMask, + appropriateFor: nil, + create: true) + } catch { + fatalError("Can't find documents directory.") + } + } @ObservedObject private var meshData : MeshData @ObservedObject private var messageData : MessageData @@ -20,6 +31,8 @@ class BLEManager: NSObject, ObservableObject, CBCentralManagerDelegate, CBPeriph @Published var isSwitchedOn = false @Published var peripherals = [Peripheral]() + + private var meshLoggingEnabled: Bool = true private var broadcastNodeId: UInt32 = 4294967295 @@ -32,6 +45,8 @@ class BLEManager: NSObject, ObservableObject, CBCentralManagerDelegate, CBPeriph let TORADIO_UUID = CBUUID(string: "0xF75C76D2-129E-4DAD-A1DD-7866124401E7") let 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") /* init BLEManager */ override init() { @@ -88,7 +103,8 @@ class BLEManager: NSObject, ObservableObject, CBCentralManagerDelegate, CBPeriph } self.centralManager?.connect(peripheral) - print("Connected to: \(peripheral.name ?? "Unknown")") + if meshLoggingEnabled { Logger.log("BLE Connecting: \(peripheral.name ?? "Unknown")") } + print("BLE Connecting: \(peripheral.name ?? "Unknown")") } // Disconnect Device function @@ -122,63 +138,14 @@ class BLEManager: NSObject, ObservableObject, CBCentralManagerDelegate, CBPeriph let deviceName = peripheral.name ?? "" let peripheralLast4: String = String(deviceName.suffix(4)) + print(peripheralLast4) connectedNode = meshData.nodes.first(where: { $0.user.id.contains(peripheralLast4) }) lastConnectedPeripheral = peripheral.identifier.uuidString peripheral.discoverServices([meshtasticServiceCBUUID]) - print("Peripheral connected: " + peripheral.name!) - } - - // Send Broadcast Message - public func sendMessage(message: String) -> 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.connectedNode == nil { - - // Try and connect to the last connected device - self.disconnectDevice() - let lastConnectedPeripheral = peripherals.filter({ $0.peripheral.identifier.uuidString == self.lastConnectedPeripheral }).first - if lastConnectedPeripheral != nil && lastConnectedPeripheral?.peripheral != nil { - connectTo(peripheral: lastConnectedPeripheral!.peripheral) - } - success = false - success = false - } - else if message.count < 1 { - // Don's send an empty message - success = false - } - else { - - let messageModel = MessageModel(messageId: 0, messageTimeStamp: UInt32(Date().timeIntervalSince1970), fromUserId: self.connectedNode.num, toUserId: broadcastNodeId, fromUserLongName: self.connectedNode.user.longName, toUserLongName: "Broadcast", fromUserShortName: self.connectedNode.user.shortName, toUserShortName: "BC", receivedACK: false, messagePayload: message, direction: "OUT") - let dataType = PortNum.textMessageApp - let payloadData: Data = message.data(using: String.Encoding.utf8)! - - var dataMessage = DataMessage() - dataMessage.payload = payloadData - dataMessage.portnum = dataType - - var meshPacket = MeshPacket() - meshPacket.to = broadcastNodeId - meshPacket.decoded = dataMessage - meshPacket.wantAck = true - - 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) - messageData.messages.append(messageModel) - messageData.save() - success = true - } - } - return success + if meshLoggingEnabled { Logger.log("BLE Connected: \(peripheral.name ?? "Unknown")") } + print("BLE Connected: \(peripheral.name ?? "Unknown")") + peripherals.removeAll() } // Disconnect Peripheral Event @@ -188,15 +155,17 @@ class BLEManager: NSObject, ObservableObject, CBCentralManagerDelegate, CBPeriph self.startScanning() if let e = error { - print("Central disconnected because \(e)") + let errorCode = (e as NSError).code if errorCode == 6 { // The connection has timed out unexpectedly. // Happens when device is manually reset / powered off // 2 second delay for device to power back on - let _ = Timer.scheduledTimer(withTimeInterval: 2.0, repeats: false) { (timer) in + let _ = Timer.scheduledTimer(withTimeInterval: 1.0, repeats: false) { (timer) in + //self.connectedPeripheral = nil + self.connectedNode = nil self.connectTo(peripheral: peripheral) } } @@ -204,25 +173,28 @@ class BLEManager: NSObject, ObservableObject, CBCentralManagerDelegate, CBPeriph // Seems to be what is received when a tbeam sleeps, immediately recconnecting does not work. // Check if the last connected peripheral is still visible and then reconnect - connectedPeripheral = nil - connectedNode = nil + connectedPeripheral = nil + connectedNode = nil + } else if errorCode == 14 { // Peer error that may require forgetting device in settings // Forgetting and reconnecting seems to be necessary so we need to show the user an error telling them to do that + connectedPeripheral = nil + connectedNode = nil lastConnectionError = (e as NSError).description - connectedPeripheral = nil - connectedNode = nil } - + print("Central disconnected because \(e)") + if meshLoggingEnabled { Logger.log("BLE Disconnected: \(peripheral.name ?? "Unknown") Error Code: \(errorCode) Error: \(e.localizedDescription)") } } else { // Disconnected without error which indicates user intent to disconnect + connectedPeripheral = nil + connectedNode = nil + if meshLoggingEnabled { Logger.log("BLE Disconnected: \(peripheral.name ?? "Unknown"): User Initiated Disconnect" ) } print("Central disconnected! (no error)") - connectedPeripheral = nil - connectedNode = nil } - + print("Peripheral disconnected: " + peripheral.name!) } @@ -238,11 +210,12 @@ class BLEManager: NSObject, ObservableObject, CBCentralManagerDelegate, CBPeriph for service in services { - print("Service discovered: " + service.uuid.uuidString) + if (service.uuid == meshtasticServiceCBUUID) { - print ("Meshtastic service OK") + print("Meshtastic service discovered OK") + if meshLoggingEnabled { Logger.log("BLE Service for Meshtastic discovered by \(peripheral.name ?? "Unknown")") } //peripheral.discoverCharacteristics(nil, for: service) peripheral.discoverCharacteristics([TORADIO_UUID, FROMRADIO_UUID, FROMNUM_UUID], for: service) } @@ -255,6 +228,7 @@ class BLEManager: NSObject, ObservableObject, CBCentralManagerDelegate, CBPeriph if let e = error { print("Discover Characteristics error \(e)") + if meshLoggingEnabled { Logger.log("BLE didDiscoverCharacteristicsFor error by \(peripheral.name ?? "Unknown") \(e)") } } guard let characteristics = service.characteristics else { return } @@ -265,6 +239,7 @@ class BLEManager: NSObject, ObservableObject, CBCentralManagerDelegate, CBPeriph { case TORADIO_UUID: print("TORADIO characteristic OK") + if meshLoggingEnabled { Logger.log("BLE did discover TORADIO characteristic for Meshtastic by \(peripheral.name ?? "Unknown")") } TORADIO_characteristic = characteristic var toRadio: ToRadio = ToRadio() toRadio.wantConfigID = 32168 @@ -274,12 +249,14 @@ class BLEManager: NSObject, ObservableObject, CBCentralManagerDelegate, CBPeriph case FROMRADIO_UUID: print("FROMRADIO characteristic OK") + if meshLoggingEnabled { Logger.log("BLE did discover FROMRADIO characteristic for Meshtastic by \(peripheral.name ?? "Unknown")") } FROMRADIO_characteristic = characteristic peripheral.readValue(for: FROMRADIO_characteristic) break case FROMNUM_UUID: print("FROMNUM (Notify) characteristic OK") + if meshLoggingEnabled { Logger.log("BLE did discover FROMNUM (Notify) characteristic for Meshtastic by \(peripheral.name ?? "Unknown")") } FROMNUM_characteristic = characteristic peripheral.setNotifyValue(true, for: characteristic) break @@ -297,6 +274,7 @@ class BLEManager: NSObject, ObservableObject, CBCentralManagerDelegate, CBPeriph if let e = error { print("didUpdateValueFor Characteristic error \(e)") + if meshLoggingEnabled { Logger.log("BLE didUpdateValueFor characteristic error by \(peripheral.name ?? "Unknown") \(e)") } } switch characteristic.uuid @@ -351,8 +329,8 @@ class BLEManager: NSObject, ObservableObject, CBCentralManagerDelegate, CBPeriph meshData.nodes.append(connectedNode) meshData.save() print("Saved a myInfo for \(decodedInfo.myInfo.myNodeNum)") + if meshLoggingEnabled { Logger.log("BLE FROMRADIO received and myInfo saved for \(peripheral.name ?? "Unknown")") } } - } if decodedInfo.nodeInfo.num != 0 { @@ -412,6 +390,7 @@ class BLEManager: NSObject, ObservableObject, CBCentralManagerDelegate, CBPeriph snr: decodedInfo.nodeInfo.snr) ) meshData.save() + if meshLoggingEnabled { Logger.log("BLE FROMRADIO received and nodeInfo saved for \(decodedInfo.nodeInfo.user.longName)") } } } // Handle assorted app packets @@ -425,6 +404,7 @@ class BLEManager: NSObject, ObservableObject, CBCentralManagerDelegate, CBPeriph if let messageText = String(bytes: decodedInfo.packet.decoded.payload, encoding: .utf8) { print("Message Text: \(messageText)") + if meshLoggingEnabled { Logger.log("BLE FROMRADIO received for text message app \(messageText)") } let fromUser = meshData.nodes.first(where: { $0.id == decodedInfo.packet.from }) @@ -466,11 +446,15 @@ class BLEManager: NSObject, ObservableObject, CBCentralManagerDelegate, CBPeriph content: messageText) ] manager.schedule() + if meshLoggingEnabled { Logger.log("iOS Notification Scheduled for text message from \(fromUser?.user.longName ?? "Unknown") \(messageText)") } } } else if decodedInfo.packet.decoded.portnum == PortNum.nodeinfoApp { var updatedNode = meshData.nodes.first(where: {$0.id == decodedInfo.packet.from }) + if updatedNode != nil { + if meshLoggingEnabled { Logger.log("BLE FROMRADIO received for node info app for \(updatedNode!.user.longName)") } + } if updatedNode != nil { updatedNode!.snr = decodedInfo.packet.rxSnr @@ -480,6 +464,7 @@ class BLEManager: NSObject, ObservableObject, CBCentralManagerDelegate, CBPeriph meshData.nodes.remove(at: nodeIndex!) meshData.nodes.append(updatedNode!) meshData.save() + if meshLoggingEnabled { Logger.log("Updated NodeInfo SNR and Time from Node Info App Packet For: \(updatedNode!.user.longName)") } print("Updated NodeInfo SNR and Time from Packet For: \(updatedNode!.user.longName)") } } @@ -496,41 +481,119 @@ class BLEManager: NSObject, ObservableObject, CBCentralManagerDelegate, CBPeriph meshData.nodes.append(updatedNode!) meshData.save() + if meshLoggingEnabled { Logger.log("Updated NodeInfo SNR and Time from Position App Packet For: \(updatedNode!.user.longName)") } print("Updated Position SNR and Time from Packet For: \(updatedNode!.user.longName)") } - print("Postion Payload") - print(try decodedInfo.packet.jsonString()) + print("Postion Payload") + print(try decodedInfo.packet.jsonString()) } - else if decodedInfo.packet.decoded.portnum == PortNum.adminApp { - - print("Admin App Packet") - print(try decodedInfo.packet.jsonString()) - } - else if decodedInfo.packet.decoded.portnum == PortNum.routingApp { - - print("Routing App Packet") - //print(try decodedInfo.packet.jsonString()) - } - else - { - print("Other App Packet") - print(try decodedInfo.packet.jsonString()) - } + else if decodedInfo.packet.decoded.portnum == PortNum.adminApp { + + if meshLoggingEnabled { Logger.log("BLE FROMRADIO received for Admin App UNHANDLED \(try decodedInfo.packet.jsonString())") } + print("Admin App Packet") + print(try decodedInfo.packet.jsonString()) + } + else if decodedInfo.packet.decoded.portnum == PortNum.routingApp { + + if meshLoggingEnabled { Logger.log("BLE FROMRADIO received for Routing App UNHANDLED \(try decodedInfo.packet.jsonString())") } + print("Routing App Packet") + print(try decodedInfo.packet.jsonString()) + } + else + { + if meshLoggingEnabled { Logger.log("BLE FROMRADIO received for Other App UNHANDLED \(try decodedInfo.packet.jsonString())") } + print("Other App Packet") + print(try decodedInfo.packet.jsonString()) + } } catch { + if meshLoggingEnabled { Logger.log("Fatal Error: Failed to decode json") } fatalError("Failed to decode json") } } if decodedInfo.configCompleteID != 0 { - print("Config Complete: \(decodedInfo)") + if meshLoggingEnabled { Logger.log("Config Complete: \(decodedInfo.configCompleteID)") } + print("BLE Config Complete Packet Id: \(decodedInfo.configCompleteID)") } default: + + if meshLoggingEnabled { Logger.log("Unhandled Characteristic UUID: \(characteristic.uuid)") } print("Unhandled Characteristic UUID: \(characteristic.uuid)") } peripheral.readValue(for: FROMRADIO_characteristic) } + + // Send Broadcast Message + public func sendMessage(message: String) -> 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.disconnectDevice() + 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 { + connectTo(peripheral: preferredPeripheral!.peripheral) + } else { + + // Try and connect to the last connected device + let lastConnectedPeripheral = peripherals.filter({ $0.peripheral.identifier.uuidString == self.lastConnectedPeripheral }).first + if lastConnectedPeripheral != nil && lastConnectedPeripheral?.peripheral != nil { + connectTo(peripheral: lastConnectedPeripheral!.peripheral) + } + } + success = false + } + else if message.count < 1 { + // Don's send an empty message + success = false + } + else { + + var longName: String = self.connectedPeripheral.name + var shortName: String = "???" + var nodeNum: UInt32 = self.connectedPeripheral.myInfo?.myNodeNum ?? 0 + + if connectedNode != nil { + longName = connectedNode.user.longName + shortName = connectedNode.user.shortName + nodeNum = connectedNode.num + } + + let messageModel = MessageModel(messageId: 0, messageTimeStamp: UInt32(Date().timeIntervalSince1970), fromUserId: nodeNum, toUserId: broadcastNodeId, fromUserLongName: longName, toUserLongName: "Broadcast", fromUserShortName: shortName, toUserShortName: "BC", receivedACK: false, messagePayload: message, direction: "OUT") + let dataType = PortNum.textMessageApp + let payloadData: Data = message.data(using: String.Encoding.utf8)! + + var dataMessage = DataMessage() + dataMessage.payload = payloadData + dataMessage.portnum = dataType + + var meshPacket = MeshPacket() + meshPacket.to = broadcastNodeId + meshPacket.decoded = dataMessage + meshPacket.wantAck = true + + 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) + messageData.messages.append(messageModel) + messageData.save() + success = true + } + } + return success + } } diff --git a/MeshtasticClient/Helpers/Logger.swift b/MeshtasticClient/Helpers/Logger.swift new file mode 100644 index 00000000..dabee233 --- /dev/null +++ b/MeshtasticClient/Helpers/Logger.swift @@ -0,0 +1,31 @@ +import Foundation + +class Logger { + + static var logFile: URL? { + guard let documentsDirectory = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first else { return nil } + let fileName = "mesh.log" + return documentsDirectory.appendingPathComponent(fileName) + } + + static func log(_ message: String) { + guard let logFile = logFile else { + return + } + + let formatter = DateFormatter() + formatter.dateFormat = "M/d/yy h:mm:ss.SSSS" + let timestamp = formatter.string(from: Date()) + guard let data = (message + " - " + timestamp + "\n").data(using: String.Encoding.utf8) else { return } + + if FileManager.default.fileExists(atPath: logFile.path) { + if let fileHandle = try? FileHandle(forWritingTo: logFile) { + fileHandle.seekToEndOfFile() + fileHandle.write(data) + fileHandle.closeFile() + } + } else { + try? data.write(to: logFile, options: .atomicWrite) + } + } +} diff --git a/MeshtasticClient/Model/NodeInfoModel.swift b/MeshtasticClient/Model/NodeInfoModel.swift index e804d415..a0f45c9d 100644 --- a/MeshtasticClient/Model/NodeInfoModel.swift +++ b/MeshtasticClient/Model/NodeInfoModel.swift @@ -93,8 +93,8 @@ extension NodeInfoModel { static var data: [NodeInfoModel] { [ - NodeInfoModel(num: 2792101487, user: User(id: "!a66c166f", longName: "RAK Solar 2", shortName: "RS2", hwModel: "RAK4631"), position: Position(latitudeI:nil, longitudeI: nil, altitude: nil, batteryLevel: 68, time: nil), lastHeard: 1631593661, snr: nil), - NodeInfoModel(num: 1000569662, user: User(id: "!3ba37b3e", longName: "RAK Solar 1", shortName: "RS1", hwModel: "RAK4631"), position: Position(latitudeI:476021390, longitudeI: -1221532609, altitude: 71, batteryLevel: 70, time: 1632202227), lastHeard: 1632202227, snr: 5.25) + // NodeInfoModel(num: 2792101487, user: User(id: "!a66c166f", longName: "RAK Solar 2", shortName: "RS2", hwModel: "RAK4631"), position: Position(latitudeI:nil, longitudeI: nil, altitude: nil, batteryLevel: 68, time: nil), lastHeard: 1631593661, snr: nil), + // NodeInfoModel(num: 1000569662, user: User(id: "!3ba37b3e", longName: "RAK Solar 1", shortName: "RS1", hwModel: "RAK4631"), position: Position(latitudeI:476021390, longitudeI: -1221532609, altitude: 71, batteryLevel: 70, time: 1632202227), lastHeard: 1632202227, snr: 5.25) ] } } diff --git a/MeshtasticClient/Views/Bluetooth/Connect.swift b/MeshtasticClient/Views/Bluetooth/Connect.swift index 022c0288..b3930589 100644 --- a/MeshtasticClient/Views/Bluetooth/Connect.swift +++ b/MeshtasticClient/Views/Bluetooth/Connect.swift @@ -33,53 +33,53 @@ struct Connect: View { Section(header: Text("Connected Device").font(.title)) { if bleManager.connectedPeripheral != nil && bleManager.connectedPeripheral.peripheral.state == .connected { HStack { + Image(systemName: "antenna.radiowaves.left.and.right") .symbolRenderingMode(.hierarchical) .imageScale(.large).foregroundColor(.green) .padding(.trailing) - if bleManager.connectedPeripheral.myInfo != nil { - VStack (alignment: .leading) { - if bleManager.connectedNode != nil { - - Text(bleManager.connectedNode.user.longName).font(.title2) - } - else { - Text(String(bleManager.connectedPeripheral.myInfo?.myNodeNum ?? 0)).font(.title2) - - } - Text("FW Version: ").font(.caption)+Text(bleManager.connectedPeripheral.myInfo?.firmwareVersion ?? "(null)").font(.caption).foregroundColor(Color.gray) - } - Spacer() - VStack (alignment: .center) { - Text("Preferred").font(.caption2) - Text("Radio").font(.caption2) - Toggle("Preferred Radio", isOn: $isPreferredRadio) - .toggleStyle(SwitchToggleStyle(tint: .accentColor)) - .labelsHidden() - .onChange(of: isPreferredRadio) { value in - if value { - if bleManager.connectedNode != nil { - userSettings.preferredPeripheralName = "\(bleManager.connectedNode.user.longName) / \(bleManager.connectedPeripheral.peripheral.name ?? "")" - } - else { - - userSettings.preferredPeripheralName = bleManager.connectedPeripheral.peripheral.name ?? "Unknown Device" - } - - userSettings.preferredPeripheralId = bleManager.connectedPeripheral!.peripheral.identifier.uuidString - - } else { - - userSettings.preferredPeripheralId = "" - userSettings.preferredPeripheralName = "" - } - } + VStack (alignment: .leading) { + if bleManager.connectedNode != nil { + + Text(bleManager.connectedNode.user.longName).font(.title2) } - } - else { - Text((bleManager.connectedPeripheral!.peripheral.name != nil) ? bleManager.connectedPeripheral!.peripheral.name! : "Unknown").font(.title2) - } + else { + + Text(String(bleManager.connectedPeripheral.peripheral.name ?? "Unknown")).font(.title2) + } + Text("Model: ").font(.caption)+Text(bleManager.connectedNode?.user.hwModel ?? "(null)").font(.caption).foregroundColor(Color.gray) + if bleManager.connectedNode != nil { + Text("BLE Name: ").font(.caption)+Text(bleManager.connectedPeripheral.name).font(.caption).foregroundColor(Color.gray) + } + Text("FW Version: ").font(.caption)+Text(bleManager.connectedPeripheral.myInfo?.firmwareVersion ?? "(null)").font(.caption).foregroundColor(Color.gray) + } + Spacer() + + VStack (alignment: .center) { + Text("Preferred").font(.caption2) + Text("Radio").font(.caption2) + Toggle("Preferred Radio", isOn: $isPreferredRadio) + .toggleStyle(SwitchToggleStyle(tint: .accentColor)) + .labelsHidden() + .onChange(of: isPreferredRadio) { value in + if value { + if bleManager.connectedNode != nil { + let deviceName = "\(bleManager.connectedNode.user.longName) / \(bleManager.connectedPeripheral.peripheral.name ?? "")" + UserDefaults.standard.set(deviceName, forKey: "preferredPeripheralName") + //userSettings.preferredPeripheralName = "\(bleManager.connectedNode.user.longName) / \(bleManager.connectedPeripheral.peripheral.name ?? "")" + } else { + UserDefaults.standard.set(bleManager.connectedPeripheral.peripheral.name ?? "Unknown Device", forKey: "preferredPeripheralName") + //userSettings.preferredPeripheralName = + } + UserDefaults.standard.set(bleManager.connectedPeripheral!.peripheral.identifier.uuidString, forKey: "preferredPeripheralId") + + } else { + UserDefaults.standard.set("", forKey: "preferredPeripheralId") + UserDefaults.standard.set("", forKey: "preferredPeripheralName") + } + } + } } .padding([.top, .bottom]) .swipeActions { @@ -193,7 +193,9 @@ struct Connect: View { } else { isPreferredRadio = false - bleManager.startScanning() + if bleManager.connectedPeripheral == nil { + bleManager.startScanning() + } } } ) } diff --git a/MeshtasticClient/Views/Messages/Channels.swift b/MeshtasticClient/Views/Messages/Channels.swift index 2b09666e..ca3e0b97 100644 --- a/MeshtasticClient/Views/Messages/Channels.swift +++ b/MeshtasticClient/Views/Messages/Channels.swift @@ -3,17 +3,14 @@ import SwiftUI import CoreBluetooth struct Channels: View { - - // Message Data and Bluetooth - @EnvironmentObject var messageData: MessageData - @EnvironmentObject var bleManager: BLEManager - + @State private var isShowingDetailView = true + var body: some View { NavigationView { GeometryReader { bounds in - NavigationLink(destination: Messages()) { + NavigationLink(destination: Messages(), isActive: $isShowingDetailView) { List{ diff --git a/MeshtasticClient/Views/Messages/Messages.swift b/MeshtasticClient/Views/Messages/Messages.swift index 0d2c6c82..c7c6470b 100644 --- a/MeshtasticClient/Views/Messages/Messages.swift +++ b/MeshtasticClient/Views/Messages/Messages.swift @@ -9,9 +9,6 @@ struct Messages: View { case messageText } - @ObservedObject var userSettings = UserSettings() - - // Keyboard State @State var typingMessage: String = "" @State private var totalBytes = 0 @@ -119,7 +116,7 @@ struct Messages: View { ZStack { //let kbType = Enum.Parse(typeof(KeyboardType), userSettings.keyboardType, true); - let kbType = UIKeyboardType(rawValue: userSettings.keyboardType) + let kbType = UIKeyboardType(rawValue: UserDefaults.standard.object(forKey: "keyboardType") as? Int ?? 0) TextEditor(text: $typingMessage) .onChange(of: typingMessage, perform: { value in diff --git a/MeshtasticClient/Views/Nodes/NodeList.swift b/MeshtasticClient/Views/Nodes/NodeList.swift index 2e2f77c6..62b412ea 100644 --- a/MeshtasticClient/Views/Nodes/NodeList.swift +++ b/MeshtasticClient/Views/Nodes/NodeList.swift @@ -78,9 +78,6 @@ struct NodeList: View { } .ignoresSafeArea(.all, edges: [.leading, .trailing]) .navigationViewStyle(DoubleColumnNavigationViewStyle()) - .onAppear{ - meshData.load() - } } } diff --git a/MeshtasticClient/Views/Settings/AppSettings.swift b/MeshtasticClient/Views/Settings/AppSettings.swift index 2d4ec137..f33d6b2a 100644 --- a/MeshtasticClient/Views/Settings/AppSettings.swift +++ b/MeshtasticClient/Views/Settings/AppSettings.swift @@ -56,6 +56,11 @@ class UserSettings: ObservableObject { UserDefaults.standard.set(keyboardType, forKey: "keyboardType") } } + @Published var meshActivityLog: Bool { + didSet { + UserDefaults.standard.set(meshActivityLog, forKey: "meshActivityLog") + } + } init() { self.username = UserDefaults.standard.object(forKey: "username") as? String ?? "" @@ -63,6 +68,7 @@ class UserSettings: ObservableObject { self.preferredPeripheralId = UserDefaults.standard.object(forKey: "preferredPeripheralId") as? String ?? "" self.provideLocation = UserDefaults.standard.object(forKey: "provideLocation") as? Bool ?? false self.keyboardType = UserDefaults.standard.object(forKey: "keyboardType") as? Int ?? 0 + self.meshActivityLog = UserDefaults.standard.object(forKey: "meshActivityLog") as? Bool ?? false } } @@ -72,6 +78,10 @@ struct AppSettings: View { @EnvironmentObject var bleManager: BLEManager @ObservedObject var userSettings = UserSettings() + var perferredPeripheral: String { + UserDefaults.standard.object(forKey: "preferredPeripheralName") as? String ?? "" + } + var body: some View { NavigationView { @@ -115,11 +125,26 @@ struct AppSettings: View { } .pickerStyle(DefaultPickerStyle()) } + Section(header: Text("MESH NETWORK OPTIONS")) { + Toggle(isOn: $userSettings.meshActivityLog) { + + Label("Log all Mesh activity", systemImage: "network") + } + .toggleStyle(SwitchToggleStyle(tint: .accentColor)) + if userSettings.meshActivityLog { + NavigationLink(destination: MeshLog()) { + Text("View Mesh Log") + } + .listRowSeparator(.visible) + } + + } } } .navigationTitle("App Settings") } - .navigationViewStyle(StackNavigationViewStyle()) } + .navigationViewStyle(StackNavigationViewStyle()) + } } struct AppSettings_Previews: PreviewProvider { diff --git a/MeshtasticClient/Views/Settings/MeshLog.swift b/MeshtasticClient/Views/Settings/MeshLog.swift new file mode 100644 index 00000000..77fccd1b --- /dev/null +++ b/MeshtasticClient/Views/Settings/MeshLog.swift @@ -0,0 +1,70 @@ +import SwiftUI +import Foundation + +struct MeshLog: View { + let logFile = Logger.logFile + var text = "" + @State private var logs = [String]() + + var body: some View { + + List(logs, id: \.self, rowContent: Text.init) + .task { + do { + + let url = logFile! + logs.removeAll() + for try await log in url.lines { + logs.append(log) + } + logs.reverse() + } catch { + // Stop adding logs when an error is thrown + } + } + .textSelection(.enabled) + .font(.caption2) + + HStack (alignment: .center) { + Spacer() + Button(action: { + let text = "" + do { + try text.write(to: logFile!, atomically: false, encoding: .utf8) + logs.removeAll() + } catch { + print(error) + } + + }) { + Image(systemName: "trash").imageScale(.large).foregroundColor(.gray) + Text("Clear Log").font(.caption) + .font(.caption) + .foregroundColor(.gray) + } + .padding() + .background(Color(.systemGray6)) + .clipShape(Capsule()) + + Spacer() + + Button(action: { + + }) { + Image(systemName: "arrow.down.circle.fill").imageScale(.large).foregroundColor(.gray) + Text("Download Log") + .font(.caption) + .foregroundColor(.gray) + } + .padding() + .background(Color(.systemGray6)) + .clipShape(Capsule()) + .hidden() + + Spacer() + + } + .padding(.bottom, 10) + .navigationTitle("Mesh Activity Log") + } +}