diff --git a/Meshtastic Client.xcodeproj/project.pbxproj b/Meshtastic Client.xcodeproj/project.pbxproj index 929cd725..bbad33eb 100644 --- a/Meshtastic Client.xcodeproj/project.pbxproj +++ b/Meshtastic Client.xcodeproj/project.pbxproj @@ -9,6 +9,8 @@ /* Begin PBXBuildFile section */ DD23A50F26FD1B4400D9B90C /* PeripheralModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD23A50E26FD1B4400D9B90C /* PeripheralModel.swift */; }; DD23A51326FEF5D500D9B90C /* MessageData.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD23A51226FEF5D500D9B90C /* MessageData.swift */; }; + DD2E652427679E4000E45FC5 /* NodeInfoEntityRow.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD2E652327679E4000E45FC5 /* NodeInfoEntityRow.swift */; }; + DD2E65262767A01F00E45FC5 /* NodeInfoEntityDetail.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD2E65252767A01F00E45FC5 /* NodeInfoEntityDetail.swift */; }; DD47E3CC26F0E51D00029299 /* NodeDetail.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD47E3CB26F0E51D00029299 /* NodeDetail.swift */; }; DD47E3CE26F103C600029299 /* NodeList.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD47E3CD26F103C600029299 /* NodeList.swift */; }; DD47E3D026F1073F00029299 /* NodeRow.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD47E3CF26F1073F00029299 /* NodeRow.swift */; }; @@ -19,13 +21,13 @@ 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 */; }; + DD5394FC276993AD00AD86B1 /* SwiftProtobuf in Frameworks */ = {isa = PBXBuildFile; productRef = DD5394FB276993AD00AD86B1 /* SwiftProtobuf */; }; DD8169F9271F1A6100F4AB02 /* MeshLogger.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD8169F8271F1A6100F4AB02 /* MeshLogger.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 */; }; - DD8EDE9426F97A2B00A5A10B /* SwiftProtobuf in Frameworks */ = {isa = PBXBuildFile; productRef = DD8EDE9326F97A2B00A5A10B /* SwiftProtobuf */; }; DD90860C26F684AF00DC5189 /* BatteryIcon.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD90860B26F684AF00DC5189 /* BatteryIcon.swift */; }; DD90860E26F69BAE00DC5189 /* NodeMap.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD90860D26F69BAE00DC5189 /* NodeMap.swift */; }; DD913639270DFF4C00D7ACF3 /* LocalNotificationManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD913638270DFF4C00D7ACF3 /* LocalNotificationManager.swift */; }; @@ -73,6 +75,8 @@ /* Begin PBXFileReference section */ DD23A50E26FD1B4400D9B90C /* PeripheralModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PeripheralModel.swift; sourceTree = ""; }; DD23A51226FEF5D500D9B90C /* MessageData.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageData.swift; sourceTree = ""; }; + DD2E652327679E4000E45FC5 /* NodeInfoEntityRow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NodeInfoEntityRow.swift; sourceTree = ""; }; + DD2E65252767A01F00E45FC5 /* NodeInfoEntityDetail.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NodeInfoEntityDetail.swift; sourceTree = ""; }; DD47E3CB26F0E51D00029299 /* NodeDetail.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NodeDetail.swift; sourceTree = ""; }; DD47E3CD26F103C600029299 /* NodeList.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NodeList.swift; sourceTree = ""; }; DD47E3CF26F1073F00029299 /* NodeRow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NodeRow.swift; sourceTree = ""; }; @@ -128,7 +132,7 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( - DD8EDE9426F97A2B00A5A10B /* SwiftProtobuf in Frameworks */, + DD5394FC276993AD00AD86B1 /* SwiftProtobuf in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -156,6 +160,8 @@ DD47E3CD26F103C600029299 /* NodeList.swift */, DD47E3CF26F1073F00029299 /* NodeRow.swift */, DD90860D26F69BAE00DC5189 /* NodeMap.swift */, + DD2E652327679E4000E45FC5 /* NodeInfoEntityRow.swift */, + DD2E65252767A01F00E45FC5 /* NodeInfoEntityDetail.swift */, ); path = Nodes; sourceTree = ""; @@ -360,7 +366,7 @@ ); name = MeshtasticClient; packageProductDependencies = ( - DD8EDE9326F97A2B00A5A10B /* SwiftProtobuf */, + DD5394FB276993AD00AD86B1 /* SwiftProtobuf */, ); productName = MeshtasticClient; productReference = DDC2E15426CE248E0042C5E4 /* MeshtasticClient.app */; @@ -434,7 +440,7 @@ ); mainGroup = DDC2E14B26CE248E0042C5E4; packageReferences = ( - DDAF8C5926ED08D30058C060 /* XCRemoteSwiftPackageReference "swift-protobuf" */, + DD5394FA276993AD00AD86B1 /* XCRemoteSwiftPackageReference "swift-protobuf" */, ); productRefGroup = DDC2E15526CE248E0042C5E4 /* Products */; projectDirPath = ""; @@ -507,6 +513,7 @@ DD913639270DFF4C00D7ACF3 /* LocalNotificationManager.swift in Sources */, DDAF8C5326EB1DF10058C060 /* BLEManager.swift in Sources */, DDC4D568275499A500A4208E /* Persistence.swift in Sources */, + DD2E652427679E4000E45FC5 /* NodeInfoEntityRow.swift in Sources */, DD90860E26F69BAE00DC5189 /* NodeMap.swift in Sources */, DD47E3DB26F3901B00029299 /* Channels.swift in Sources */, DD8169FF272476C700F4AB02 /* LogDocument.swift in Sources */, @@ -531,6 +538,7 @@ DDAF8C6326ED0A230058C060 /* admin.pb.swift in Sources */, DDAF8C5826ED07FD0058C060 /* mesh.pb.swift in Sources */, DD8169FB271F1F3A00F4AB02 /* MeshLog.swift in Sources */, + DD2E65262767A01F00E45FC5 /* NodeInfoEntityDetail.swift in Sources */, DD47E3D926F3093800029299 /* MessageBubble.swift in Sources */, DD8169F9271F1A6100F4AB02 /* MeshLogger.swift in Sources */, DDAF8C6726ED0C8C0058C060 /* remote_hardware.pb.swift in Sources */, @@ -870,20 +878,20 @@ /* End XCConfigurationList section */ /* Begin XCRemoteSwiftPackageReference section */ - DDAF8C5926ED08D30058C060 /* XCRemoteSwiftPackageReference "swift-protobuf" */ = { + DD5394FA276993AD00AD86B1 /* XCRemoteSwiftPackageReference "swift-protobuf" */ = { isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/apple/swift-protobuf.git"; requirement = { kind = upToNextMajorVersion; - minimumVersion = 1.17.0; + minimumVersion = 1.0.0; }; }; /* End XCRemoteSwiftPackageReference section */ /* Begin XCSwiftPackageProductDependency section */ - DD8EDE9326F97A2B00A5A10B /* SwiftProtobuf */ = { + DD5394FB276993AD00AD86B1 /* SwiftProtobuf */ = { isa = XCSwiftPackageProductDependency; - package = DDAF8C5926ED08D30058C060 /* XCRemoteSwiftPackageReference "swift-protobuf" */; + package = DD5394FA276993AD00AD86B1 /* XCRemoteSwiftPackageReference "swift-protobuf" */; productName = SwiftProtobuf; }; /* End XCSwiftPackageProductDependency section */ diff --git a/Meshtastic Client.xcodeproj/xcuserdata/garthvanderhouwen.xcuserdatad/xcdebugger/Breakpoints_v2.xcbkptlist b/Meshtastic Client.xcodeproj/xcuserdata/garthvanderhouwen.xcuserdatad/xcdebugger/Breakpoints_v2.xcbkptlist index 4574a97c..4612af2b 100644 --- a/Meshtastic Client.xcodeproj/xcuserdata/garthvanderhouwen.xcuserdatad/xcdebugger/Breakpoints_v2.xcbkptlist +++ b/Meshtastic Client.xcodeproj/xcuserdata/garthvanderhouwen.xcuserdatad/xcdebugger/Breakpoints_v2.xcbkptlist @@ -65,18 +65,82 @@ + + + + + + + + + + + + + + + + diff --git a/MeshtasticClient/Helpers/BLEManager.swift b/MeshtasticClient/Helpers/BLEManager.swift index f7fa7e82..d2e979f9 100644 --- a/MeshtasticClient/Helpers/BLEManager.swift +++ b/MeshtasticClient/Helpers/BLEManager.swift @@ -19,12 +19,13 @@ class BLEManager: NSObject, ObservableObject, CBCentralManagerDelegate, CBPeriph } var context: NSManagedObjectContext? + private var centralManager: CBCentralManager! @Published var peripherals = [Peripheral]() @Published var connectedPeripheral: Peripheral! - @Published var connectedNode: NodeInfoModel! + @Published var connectedNode: NodeInfoEntity! @Published var lastConnectedPeripheral: String @Published var lastConnectionError: String @@ -49,25 +50,16 @@ class BLEManager: NSObject, ObservableObject, CBCentralManagerDelegate, CBPeriph private var meshLoggingEnabled: Bool = false let meshLog = documentsFolder.appendingPathComponent("meshlog.txt") - - - //Eventually Delete - @Published var meshData: MeshData - //@Published var messageData: MessageData // MARK: init BLEManager override init() { self.meshLoggingEnabled = UserDefaults.standard.object(forKey: "meshActivityLog") as? Bool ?? false - self.meshData = MeshData() - //self.messageData = MessageData() self.lastConnectedPeripheral = "" self.lastConnectionError = "" super.init() //let bleQueue: DispatchQueue = DispatchQueue(label: "CentralManager") centralManager = CBCentralManager(delegate: self, queue: nil) - meshData.load() - //messageData.load() } // MARK: Bluetooth enabled/disabled for the app @@ -82,7 +74,6 @@ class BLEManager: NSObject, ObservableObject, CBCentralManagerDelegate, CBPeriph } } - // MARK: Scanning for BLE Devices // Scan for nearby BLE devices using the Meshtastic BLE service ID func startScanning() { @@ -113,8 +104,8 @@ class BLEManager: NSObject, ObservableObject, CBCentralManagerDelegate, CBPeriph /// - timer: The time that fired the event /// @objc func timeoutTimerFired(timer: Timer) { - guard let context = timer.userInfo as? [String: String] else { return } - let name: String = context["name", default: "Unknown"] + guard let timerContext = timer.userInfo as? [String: String] else { return } + let name: String = timerContext["name", default: "Unknown"] self.timeoutTimerCount += 1 @@ -124,7 +115,6 @@ class BLEManager: NSObject, ObservableObject, CBCentralManagerDelegate, CBPeriph self.centralManager?.cancelPeripheralConnection(connectedPeripheral.peripheral) } - connectedNode = nil connectedPeripheral = nil self.lastConnectionError = "BLE Connecting Timeout after making \(timeoutTimerCount) attempts to connect to \(name)." @@ -177,12 +167,12 @@ class BLEManager: NSObject, ObservableObject, CBCentralManagerDelegate, CBPeriph peripheralName = name } - var newPeripheral = Peripheral(id: peripheral.identifier.uuidString, name: peripheralName, rssi: RSSI.intValue, subscribed: false, peripheral: peripheral, myInfo: nil) + let newPeripheral = Peripheral(id: peripheral.identifier.uuidString, name: peripheralName, firmwareVersion: "Unknown", rssi: RSSI.intValue, subscribed: false, peripheral: peripheral, myInfo: nil) let peripheralIndex = peripherals.firstIndex(where: { $0.id == newPeripheral.id }) if peripheralIndex != nil && newPeripheral.peripheral.state != CBPeripheralState.connected { - newPeripheral.myInfo = peripherals.first(where: { $0.id == newPeripheral.id })?.myInfo + //newPeripheral.myInfo = peripherals.first(where: { $0.id == newPeripheral.id })?.myInfo peripherals[peripheralIndex!] = newPeripheral peripherals.remove(at: peripheralIndex!) peripherals.append(newPeripheral) @@ -211,9 +201,23 @@ class BLEManager: NSObject, ObservableObject, CBCentralManagerDelegate, CBPeriph // Map the peripheral to the connectedNode and connectedPeripheral ObservedObjects connectedPeripheral = peripherals.filter({ $0.peripheral.identifier == peripheral.identifier }).first connectedPeripheral.peripheral.delegate = self - let deviceName = peripheral.name ?? "" - let peripheralLast4: String = String(deviceName.suffix(4)) - connectedNode = self.meshData.nodes.first(where: { $0.user.id.contains(peripheralLast4) }) + + let fetchNodeRequest:NSFetchRequest = NSFetchRequest.init(entityName: "NodeInfoEntity") + fetchNodeRequest.predicate = NSPredicate(format: "bleName MATCHES %@", String(peripheral.name ?? "???")) + + do { + let fetchedNode = try context?.fetch(fetchNodeRequest) as! [NodeInfoEntity] + + if fetchedNode.count == 1 { + + connectedPeripheral.name = fetchedNode[0].user!.longName! + connectedPeripheral.firmwareVersion = (fetchedNode[0].myInfo?.firmwareVersion ?? "Unknown") + } + + } catch { + print("Fetch NodeInfo Failed") + } + lastConnectedPeripheral = peripheral.identifier.uuidString // Discover Services @@ -257,19 +261,19 @@ class BLEManager: NSObject, ObservableObject, CBCentralManagerDelegate, CBPeriph // Seems to be what is received when a tbeam sleeps, immediately recconnecting does not work. lastConnectionError = e.localizedDescription - self.connectedNode = nil + print("BLE Disconnected: \(peripheral.name ?? "Unknown") Error Code: \(errorCode) Error: \(e.localizedDescription)") if meshLoggingEnabled { MeshLogger.log("BLE Disconnected: \(peripheral.name ?? "Unknown") Error Code: \(errorCode) Error: \(e.localizedDescription)") } } else if errorCode == 14 { // Peer removed pairing information // Forgetting and reconnecting seems to be necessary so we need to show the user an error telling them to do that lastConnectionError = "\(e.localizedDescription) This error usually cannot be fixed without forgetting the device unders Settings > Bluetooth and re-connecting to the radio." - self.connectedNode = nil + if meshLoggingEnabled { MeshLogger.log("BLE Disconnected: \(peripheral.name ?? "Unknown") Error Code: \(errorCode) Error: \(lastConnectionError)") } } else { lastConnectionError = e.localizedDescription - self.connectedNode = nil + print("BLE Disconnected: \(peripheral.name ?? "Unknown") Error Code: \(errorCode) Error: \(e.localizedDescription)") if meshLoggingEnabled { MeshLogger.log("BLE Disconnected: \(peripheral.name ?? "Unknown") Error Code: \(errorCode) Error: \(e.localizedDescription)") } } @@ -277,7 +281,6 @@ class BLEManager: NSObject, ObservableObject, CBCentralManagerDelegate, CBPeriph // Disconnected without error which indicates user intent to disconnect // Happens when swiping to disconnect - self.connectedNode = nil if meshLoggingEnabled { MeshLogger.log("BLE Disconnected: \(peripheral.name ?? "Unknown"): User Initiated Disconnect") } print("BLE Disconnected: \(peripheral.name ?? "Unknown"): User Initiated Disconnect") } @@ -394,7 +397,6 @@ class BLEManager: NSObject, ObservableObject, CBCentralManagerDelegate, CBPeriph let fetchMyInfoRequest:NSFetchRequest = NSFetchRequest.init(entityName: "MyInfoEntity") fetchMyInfoRequest.predicate = NSPredicate(format: "myNodeNum == %i", Int64(decodedInfo.myInfo.myNodeNum)) - do { let fetchedMyInfo = try context?.fetch(fetchMyInfoRequest) as! [MyInfoEntity] // Not Found Insert @@ -407,28 +409,36 @@ class BLEManager: NSObject, ObservableObject, CBCentralManagerDelegate, CBPeriph myInfo.messageTimeoutMsec = Int32(bitPattern: decodedInfo.myInfo.messageTimeoutMsec) myInfo.minAppVersion = Int32(bitPattern: decodedInfo.myInfo.minAppVersion) myInfo.maxChannels = Int32(bitPattern: decodedInfo.myInfo.maxChannels) - - do { - - try context!.save() - print("Saved a myInfo for \(decodedInfo.myInfo.myNodeNum)") - - } catch { - - context!.rollback() - - let nsError = error as NSError - print("Error Saving CoreData MyInfoEntity: \(nsError)") - } } + else { + + fetchedMyInfo[0].myNodeNum = Int64(decodedInfo.myInfo.myNodeNum) + fetchedMyInfo[0].hasGps = decodedInfo.myInfo.hasGps_p + fetchedMyInfo[0].numBands = Int32(bitPattern: decodedInfo.myInfo.numBands) + fetchedMyInfo[0].firmwareVersion = decodedInfo.myInfo.firmwareVersion + fetchedMyInfo[0].messageTimeoutMsec = Int32(bitPattern: decodedInfo.myInfo.messageTimeoutMsec) + fetchedMyInfo[0].minAppVersion = Int32(bitPattern: decodedInfo.myInfo.minAppVersion) + fetchedMyInfo[0].maxChannels = Int32(bitPattern: decodedInfo.myInfo.maxChannels) + } + do { + + try context!.save() + print("Saved a myInfo for \(decodedInfo.myInfo.myNodeNum)") + if meshLoggingEnabled { MeshLogger.log("BLE FROMRADIO received and myInfo saved for \(peripheral.name ?? String(decodedInfo.myInfo.myNodeNum))") } + + } catch { + + context!.rollback() + + let nsError = error as NSError + print("Error Saving CoreData MyInfoEntity: \(nsError)") + } + } catch { + print("Fetch MyInfo Error") } - - - - //if meshLoggingEnabled { MeshLogger.log("BLE FROMRADIO received and myInfo saved for \(peripheral.name ?? String(myInfo.myNodeNum))") } } // NodeInfo Data @@ -437,9 +447,10 @@ class BLEManager: NSObject, ObservableObject, CBCentralManagerDelegate, CBPeriph print("Save a CoreData NodeInfoEntity") let fetchNodeRequest:NSFetchRequest = NSFetchRequest.init(entityName: "NodeInfoEntity") - fetchNodeRequest.predicate = NSPredicate(format: "num == %i", Int64(decodedInfo.nodeInfo.num)) + fetchNodeRequest.predicate = NSPredicate(format: "num == %lld", Int64(decodedInfo.nodeInfo.num)) do { + let fetchedNode = try context?.fetch(fetchNodeRequest) as! [NodeInfoEntity] // Not Found Insert if fetchedNode.isEmpty { @@ -466,19 +477,19 @@ class BLEManager: NSObject, ObservableObject, CBCentralManagerDelegate, CBPeriph newNode.user = newUser } - if decodedInfo.nodeInfo.hasPosition && decodedInfo.nodeInfo.position.latitudeI != 0 { - - let position = PositionEntity(context: context!) - position.latitudeI = decodedInfo.nodeInfo.position.latitudeI - position.longitudeI = decodedInfo.nodeInfo.position.longitudeI - position.altitude = decodedInfo.nodeInfo.position.altitude - position.batteryLevel = decodedInfo.nodeInfo.position.batteryLevel - position.time = Int32(bitPattern: decodedInfo.nodeInfo.position.time) - - var newPostions = [PositionEntity]() - newPostions.append(position) - newNode.positions? = NSOrderedSet(array : newPostions) - } +// if false && decodedInfo.nodeInfo.hasPosition && decodedInfo.nodeInfo.position.latitudeI != 0 { +// +// let position = PositionEntity(context: context!) +// position.latitudeI = decodedInfo.nodeInfo.position.latitudeI +// position.longitudeI = decodedInfo.nodeInfo.position.longitudeI +// position.altitude = decodedInfo.nodeInfo.position.altitude +// position.batteryLevel = decodedInfo.nodeInfo.position.batteryLevel +// position.time = Int32(bitPattern: decodedInfo.nodeInfo.position.time) +// +// var newPostions = [PositionEntity]() +// newPostions.append(position) +// newNode.positions? = NSOrderedSet(array : newPostions) +// } // Look for a MyInfo let fetchMyInfoRequest:NSFetchRequest = NSFetchRequest.init(entityName: "MyInfoEntity") @@ -498,44 +509,44 @@ class BLEManager: NSObject, ObservableObject, CBCentralManagerDelegate, CBPeriph } else { - let updatedNode = fetchedNode[0] - updatedNode.timestamp = Date() - updatedNode.lastHeard = Int32(bitPattern: decodedInfo.nodeInfo.lastHeard) - updatedNode.snr = decodedInfo.nodeInfo.snr + fetchedNode[0].id = Int64(decodedInfo.nodeInfo.num) + fetchedNode[0].num = Int64(decodedInfo.nodeInfo.num) + fetchedNode[0].timestamp = Date() + fetchedNode[0].lastHeard = Int32(bitPattern: decodedInfo.nodeInfo.lastHeard) + fetchedNode[0].snr = decodedInfo.nodeInfo.snr if decodedInfo.nodeInfo.hasUser { - updatedNode.user!.userId = decodedInfo.nodeInfo.user.id - updatedNode.user!.longName = decodedInfo.nodeInfo.user.longName - updatedNode.user!.shortName = decodedInfo.nodeInfo.user.shortName - updatedNode.user!.hwModel = String(describing: decodedInfo.nodeInfo.user.hwModel).uppercased() - } - if decodedInfo.nodeInfo.hasPosition { - - let position = PositionEntity(context: context!) - position.latitudeI = decodedInfo.nodeInfo.position.latitudeI - position.longitudeI = decodedInfo.nodeInfo.position.longitudeI - position.altitude = decodedInfo.nodeInfo.position.altitude - position.batteryLevel = decodedInfo.nodeInfo.position.batteryLevel - position.time = Int32(bitPattern: decodedInfo.nodeInfo.position.time) - - if position.latitudeI != 0 { - let mutablePositions = updatedNode.positions!.mutableCopy() as! NSMutableOrderedSet - mutablePositions.add(position) - updatedNode.positions = mutablePositions.copy() as? NSOrderedSet - } + fetchedNode[0].user!.userId = decodedInfo.nodeInfo.user.id + fetchedNode[0].user!.longName = decodedInfo.nodeInfo.user.longName + fetchedNode[0].user!.shortName = decodedInfo.nodeInfo.user.shortName + fetchedNode[0].user!.hwModel = String(describing: decodedInfo.nodeInfo.user.hwModel).uppercased() } +// if decodedInfo.nodeInfo.hasPosition { +// +// let position = PositionEntity(context: context!) +// position.latitudeI = decodedInfo.nodeInfo.position.latitudeI +// position.longitudeI = decodedInfo.nodeInfo.position.longitudeI +// position.altitude = decodedInfo.nodeInfo.position.altitude +// position.batteryLevel = decodedInfo.nodeInfo.position.batteryLevel +// position.time = Int32(bitPattern: decodedInfo.nodeInfo.position.time) +// +// if position.latitudeI != 0 { +// let mutablePositions = fetchedNode[0].positions!.mutableCopy() as! NSMutableOrderedSet +// mutablePositions.add(position) +// fetchedNode[0].positions = mutablePositions.copy() as? NSOrderedSet +// } +// } // Look for a MyInfo let fetchMyInfoRequest:NSFetchRequest = NSFetchRequest.init(entityName: "MyInfoEntity") - fetchMyInfoRequest.predicate = NSPredicate(format: "myNodeNum == %i", Int64(decodedInfo.nodeInfo.num)) + fetchMyInfoRequest.predicate = NSPredicate(format: "myNodeNum == %lld", Int64(decodedInfo.nodeInfo.num)) do { let fetchedMyInfo = try context?.fetch(fetchMyInfoRequest) as! [MyInfoEntity] if fetchedMyInfo.count > 0 { - updatedNode.myInfo = fetchedMyInfo[0] - + fetchedNode[0].myInfo = fetchedMyInfo[0] } } catch { @@ -570,62 +581,6 @@ class BLEManager: NSObject, ObservableObject, CBCentralManagerDelegate, CBPeriph print("BLE FROMRADIO received and nodeInfo saved for \(decodedInfo.nodeInfo.num)") if meshLoggingEnabled { MeshLogger.log("BLE FROMRADIO received and nodeInfo saved for \(decodedInfo.nodeInfo.num)") } } - -// if meshData.nodes.contains(where: {$0.id == decodedInfo.nodeInfo.num}) { -// -// // Found a matching node lets update it -// let nodeMatch = meshData.nodes.first(where: { $0.id == decodedInfo.nodeInfo.num }) -// if connectedPeripheral != nil && connectedPeripheral.myInfo?.myNodeNum == nodeMatch?.num { -// connectedNode = nodeMatch -// } -// -// if nodeMatch?.lastHeard ?? 0 < decodedInfo.nodeInfo.lastHeard && nodeMatch?.user != nil && nodeMatch?.user.longName.count ?? 0 > 0 { -// // The data coming from the device is newer -// -// let nodeIndex = meshData.nodes.firstIndex(where: { $0.id == decodedInfo.nodeInfo.num }) -// meshData.nodes.remove(at: nodeIndex!) -// meshData.save() -// -// } else { -// -// // Data is older than what the app already has -// return -// } -// } -// Set the connected node if the nodeInfo is for the connected node. -// if connectedPeripheral != nil && connectedPeripheral.myInfo?.myNodeNum == decodedInfo.nodeInfo.num { -// -// let nodeMatch = meshData.nodes.first(where: { $0.id == decodedInfo.nodeInfo.num }) -// if nodeMatch != nil { -// connectedNode = nodeMatch -// } -// } - if decodedInfo.nodeInfo.hasUser { - - meshData.nodes.append( - NodeInfoModel( - num: decodedInfo.nodeInfo.num, - user: NodeInfoModel.User( - id: decodedInfo.nodeInfo.user.id, - longName: decodedInfo.nodeInfo.user.longName, - shortName: decodedInfo.nodeInfo.user.shortName, - // macaddr: decodedInfo.nodeInfo.user.macaddr, - hwModel: String(describing: decodedInfo.nodeInfo.user.hwModel) - .uppercased()), - - position: NodeInfoModel.Position( - latitudeI: decodedInfo.nodeInfo.position.latitudeI, - longitudeI: decodedInfo.nodeInfo.position.longitudeI, - altitude: decodedInfo.nodeInfo.position.altitude, - batteryLevel: decodedInfo.nodeInfo.position.batteryLevel, - time: decodedInfo.nodeInfo.position.time), - - lastHeard: decodedInfo.nodeInfo.lastHeard, - snr: decodedInfo.nodeInfo.snr) - ) - meshData.save() - if meshLoggingEnabled { MeshLogger.log("BLE FROMRADIO received and nodeInfo saved for \(decodedInfo.nodeInfo.user.longName)") } - } } // Handle assorted app packets if decodedInfo.packet.id != 0 { @@ -650,12 +605,12 @@ class BLEManager: NSObject, ObservableObject, CBCentralManagerDelegate, CBPeriph let fetchedUsers = try context?.fetch(messageUsers) as! [UserEntity] let newMessage = MessageEntity(context: context!) - newMessage.messageId = Int32(bitPattern: decodedInfo.packet.id) + newMessage.messageId = Int64(decodedInfo.packet.id) newMessage.messageTimestamp = Int32(bitPattern: decodedInfo.packet.rxTime) newMessage.receivedACK = false newMessage.direction = "IN" - if decodedInfo.packet.to == broadcastNodeId { + if decodedInfo.packet.to == broadcastNodeId && fetchedUsers.count == 1 { let bcu: UserEntity = UserEntity(context: context!) bcu.shortName = "BC" @@ -693,11 +648,10 @@ class BLEManager: NSObject, ObservableObject, CBCentralManagerDelegate, CBPeriph } catch { - print("Something went wrong: \(error)") context!.rollback() let nsError = error as NSError - print("Unresolved error \(nsError)") + print("Failed to save new MessageEntity \(nsError)") } } catch { @@ -706,41 +660,103 @@ class BLEManager: NSObject, ObservableObject, CBCentralManagerDelegate, CBPeriph } } } else if decodedInfo.packet.decoded.portnum == PortNum.nodeinfoApp { + + //let fetchNodeInfoAppRequest:NSFetchRequest = NSFetchRequest.init(entityName: "NodeInfoEntity") + //fetchNodeInfoAppRequest.predicate = NSPredicate(format: "num == %i", Int64(decodedInfo.packet.from)) + + let fetchNodeRequest:NSFetchRequest = NSFetchRequest.init(entityName: "NodeInfoEntity") + fetchNodeRequest.predicate = NSPredicate(format: "num == %lld", Int64(decodedInfo.packet.from)) + + do { + + let fetchedNode = try context?.fetch(fetchNodeRequest) as! [NodeInfoEntity] - var updatedNode = meshData.nodes.first(where: {$0.id == decodedInfo.packet.from }) - if updatedNode != nil { - if meshLoggingEnabled { MeshLogger.log("BLE FROMRADIO received for node info app for \(updatedNode!.user.longName)") } - } - - if updatedNode != nil { - updatedNode!.snr = decodedInfo.packet.rxSnr - updatedNode!.lastHeard = decodedInfo.packet.rxTime - // updatedNode!.update(from: updatedNode!.data) - let nodeIndex = meshData.nodes.firstIndex(where: { $0.id == decodedInfo.packet.from }) - meshData.nodes.remove(at: nodeIndex!) - meshData.nodes.append(updatedNode!) - meshData.save() - if meshLoggingEnabled { MeshLogger.log("MESH PACKET 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)") + if fetchedNode.count == 1 { + fetchedNode[0].id = Int64(decodedInfo.nodeInfo.num) + fetchedNode[0].num = Int64(decodedInfo.nodeInfo.num) + fetchedNode[0].timestamp = Date() + fetchedNode[0].lastHeard = Int32(bitPattern: decodedInfo.packet.rxTime) + fetchedNode[0].snr = decodedInfo.packet.rxSnr + } + else { + return + } + do { + + try context!.save() + + if meshLoggingEnabled { + MeshLogger.log("MESH PACKET Updated NodeInfo SNR and Time from Node Info App Packet For: \(fetchedNode[0].num)") + } + print("Updated NodeInfo SNR and Time from Packet For: \(fetchedNode[0].num)") + + } catch { + + context!.rollback() + + let nsError = error as NSError + print("Error Saving NodeInfoEntity from NODEINFO_APP \(nsError)") + } + } catch { + + print("Error Fetching NodeInfoEntity for NODEINFO_APP") } + + + print(decodedInfo.packet.decoded.payload) + + + } else if decodedInfo.packet.decoded.portnum == PortNum.positionApp { - var updatedNode = meshData.nodes.first(where: {$0.id == decodedInfo.packet.from }) + let fetchNodePositionRequest:NSFetchRequest = NSFetchRequest.init(entityName: "NodeInfoEntity") + fetchNodePositionRequest.predicate = NSPredicate(format: "num == %lld", Int64(decodedInfo.packet.from)) + + do { + + let fetchedNode = try context?.fetch(fetchNodePositionRequest) as! [NodeInfoEntity] - if updatedNode != nil { - updatedNode!.snr = decodedInfo.packet.rxSnr - updatedNode!.lastHeard = decodedInfo.packet.rxTime - // updatedNode!.update(from: updatedNode!.data) - let nodeIndex = meshData.nodes.firstIndex(where: { $0.id == decodedInfo.packet.from }) - meshData.nodes.remove(at: nodeIndex!) - meshData.nodes.append(updatedNode!) - meshData.save() - - if meshLoggingEnabled { MeshLogger.log("MESH PACKET 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)") + if fetchedNode.count == 1 { + fetchedNode[0].id = Int64(decodedInfo.nodeInfo.num) + fetchedNode[0].num = Int64(decodedInfo.nodeInfo.num) + fetchedNode[0].timestamp = Date() + fetchedNode[0].lastHeard = Int32(bitPattern: decodedInfo.packet.rxTime) + fetchedNode[0].snr = decodedInfo.packet.rxSnr + } + else { + return + } + do { + + try context!.save() + + if meshLoggingEnabled { + MeshLogger.log("MESH PACKET Updated NodeInfo SNR and Time from Node Info App Packet For: \(fetchedNode[0].num)") + } + print("Updated NodeInfo SNR and Time from Packet For: \(fetchedNode[0].num)") + + } catch { + + context!.rollback() + + let nsError = error as NSError + print("Error Saving NodeInfoEntity from NODEINFO_APP \(nsError)") + } + } catch { + + print("Error Fetching NodeInfoEntity for NODEINFO_APP") } - print("Postion Payload") - print(try decodedInfo.packet.jsonString()) + + + print(decodedInfo.packet.decoded.payload) +// if meshLoggingEnabled { +// MeshLogger.log("MESH PACKET Updated NodeInfo SNR and Time from Position App Packet For: \(updatedNode.num)") +// } +// print("Updated NodeInfo SNR and Time from Packet For: \(updatedNode.num)") +// +// print("Postion Payload") +// print(try decodedInfo.packet.jsonString()) + } else if decodedInfo.packet.decoded.portnum == PortNum.adminApp { if meshLoggingEnabled { MeshLogger.log("MESH PACKET received for Admin App UNHANDLED \(try decodedInfo.packet.jsonString())") } @@ -804,17 +820,25 @@ class BLEManager: NSObject, ObservableObject, CBCentralManagerDelegate, CBPeriph success = false } else { - var longName: String = self.connectedPeripheral.name - var shortName: String = "???" - var nodeNum: UInt32 = self.connectedPeripheral.myInfo?.myNodeNum ?? 0 + let newMessage = MessageEntity(context: context!) + newMessage.messageId = Int64(bitPattern: 0) + newMessage.messageTimestamp = Int32(Date().timeIntervalSince1970) + newMessage.receivedACK = false + newMessage.direction = "IN" + - if connectedNode != nil { - longName = connectedNode.user.longName - shortName = connectedNode.user.shortName - nodeNum = connectedNode.num - } + let bcu: UserEntity = UserEntity(context: context!) + bcu.shortName = "BC" + bcu.longName = "Broadcast" + bcu.hwModel = "UNSET" + bcu.num = Int64(broadcastNodeId) + bcu.userId = "BROADCASTNODE" + newMessage.toUser = bcu + + // Set from user from query here - 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") + newMessage.messagePayload = message + let dataType = PortNum.textMessageApp let payloadData: Data = message.data(using: String.Encoding.utf8)! @@ -834,9 +858,20 @@ class BLEManager: NSObject, ObservableObject, CBCentralManagerDelegate, CBPeriph 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 + do { + + try context!.save() + print("Saved a new message for \(0)") + success = true + + } catch { + + context!.rollback() + + let nsError = error as NSError + print("Unresolved error \(nsError)") + } + } } return success diff --git a/MeshtasticClient/MeshtasticClientApp.swift b/MeshtasticClient/MeshtasticClientApp.swift index 0d63f601..348f9796 100644 --- a/MeshtasticClient/MeshtasticClientApp.swift +++ b/MeshtasticClient/MeshtasticClientApp.swift @@ -21,6 +21,15 @@ struct MeshtasticClientApp: App { .onChange(of: scenePhase) { (newScenePhase) in switch newScenePhase { case .background: + do { + + try persistenceController.container.viewContext.save() + print("Saved viewContext when the app went to the background.") + + } catch { + + print("Failed to save viewContext when the app goes to the background.") + } print("Scene is in the background") case .inactive: print("Scene is inactive") diff --git a/MeshtasticClient/Model/PeripheralModel.swift b/MeshtasticClient/Model/PeripheralModel.swift index 919d7236..695b2d14 100644 --- a/MeshtasticClient/Model/PeripheralModel.swift +++ b/MeshtasticClient/Model/PeripheralModel.swift @@ -4,15 +4,17 @@ import CoreBluetooth struct Peripheral: Identifiable { var id: String var name: String + var firmwareVersion: String var rssi: Int var subscribed: Bool var peripheral: CBPeripheral var myInfo: MyInfoModel? - init(id: String, name: String, rssi: Int, subscribed: Bool, peripheral: CBPeripheral, myInfo: MyInfoModel?) { + init(id: String, name: String, firmwareVersion: String, rssi: Int, subscribed: Bool, peripheral: CBPeripheral, myInfo: MyInfoModel?) { self.id = id self.name = name + self.firmwareVersion = firmwareVersion self.rssi = rssi self.subscribed = subscribed self.peripheral = peripheral diff --git a/MeshtasticClient/Views/Bluetooth/Connect.swift b/MeshtasticClient/Views/Bluetooth/Connect.swift index ba945f63..d7e798e6 100644 --- a/MeshtasticClient/Views/Bluetooth/Connect.swift +++ b/MeshtasticClient/Views/Bluetooth/Connect.swift @@ -50,15 +50,15 @@ struct Connect: View { VStack(alignment: .leading) { - if bleManager.connectedNode != nil { + if bleManager.connectedPeripheral != nil { - Text(bleManager.connectedNode.user.longName).font(.title2) + Text(bleManager.connectedPeripheral.name).font(.title2) } else { Text(String(bleManager.connectedPeripheral.peripheral.name ?? "Unknown")).font(.title2) } - if bleManager.connectedNode != nil { - Text("Model: ").font(.caption)+Text(bleManager.connectedNode?.user.hwModel ?? "(null)").font(.caption).foregroundColor(Color.gray) + if bleManager.connectedPeripheral != nil { + //Text("Model: ").font(.caption)+Text(bleManager.connectedNode?.user!.hwModel ?? "(null)").font(.caption).foregroundColor(Color.gray) } Text("BLE Name: ").font(.caption)+Text(bleManager.connectedPeripheral.name).font(.caption).foregroundColor(Color.gray) if bleManager.connectedPeripheral != nil { @@ -80,8 +80,8 @@ struct Connect: View { .onChange(of: isPreferredRadio) { value in if value { - if bleManager.connectedNode != nil { - let deviceName = "\(bleManager.connectedNode.user.longName) / \(bleManager.connectedPeripheral.peripheral.name ?? "")" + if bleManager.connectedPeripheral != nil { + let deviceName = (bleManager.connectedPeripheral.peripheral.name ?? "") userSettings.preferredPeripheralName = deviceName } else { @@ -201,7 +201,7 @@ struct Connect: View { ZStack { - ConnectedDevice(bluetoothOn: bleManager.isSwitchedOn, deviceConnected: bleManager.connectedPeripheral != nil, name: (bleManager.connectedNode != nil) ? bleManager.connectedNode.user.shortName : ((bleManager.connectedPeripheral != nil) ? bleManager.connectedPeripheral.name : "Unknown") ) + //ConnectedDevice(bluetoothOn: bleManager.isSwitchedOn, deviceConnected: bleManager.connectedPeripheral != nil, name: (bleManager.connectedNode != nil) ? bleManager.connectedNode.user.shortName : ((bleManager.connectedPeripheral != nil) ? bleManager.connectedPeripheral.name : "Unknown") ) } ) } diff --git a/MeshtasticClient/Views/ContentView.swift b/MeshtasticClient/Views/ContentView.swift index 98821a16..7f4a422b 100644 --- a/MeshtasticClient/Views/ContentView.swift +++ b/MeshtasticClient/Views/ContentView.swift @@ -60,6 +60,6 @@ struct ContentView: View { struct ContentView_Previews: PreviewProvider { static var previews: some View { ContentView() - .environmentObject(MeshData()) + // .environmentObject(MeshData()) } } diff --git a/MeshtasticClient/Views/Messages/Channels.swift b/MeshtasticClient/Views/Messages/Channels.swift index a749a8c1..bebcf817 100644 --- a/MeshtasticClient/Views/Messages/Channels.swift +++ b/MeshtasticClient/Views/Messages/Channels.swift @@ -10,26 +10,23 @@ struct Channels: View { NavigationView { - GeometryReader { _ in + NavigationLink(destination: Messages(), isActive: $isShowingDetailView) { - NavigationLink(destination: Messages(), isActive: $isShowingDetailView) { + List { - List { + HStack { - HStack { + Image(systemName: "dial.max.fill") + .font(.system(size: 62)) + .symbolRenderingMode(.hierarchical) + .padding(.trailing) + .foregroundColor(.accentColor) - Image(systemName: "dial.max.fill") - .font(.system(size: 62)) - .symbolRenderingMode(.hierarchical) - .padding(.trailing) - .foregroundColor(.accentColor) + Text("Primary") + .font(.largeTitle) - Text("Primary") - .font(.largeTitle) - - }.padding() - } - } + }.padding() + } } .navigationTitle("Channels") } @@ -39,8 +36,6 @@ struct Channels: View { struct MessageList_Previews: PreviewProvider { - static let meshData = MeshData() - static var previews: some View { Group { Channels() diff --git a/MeshtasticClient/Views/Messages/Messages.swift b/MeshtasticClient/Views/Messages/Messages.swift index 3a62710d..5bb14b74 100644 --- a/MeshtasticClient/Views/Messages/Messages.swift +++ b/MeshtasticClient/Views/Messages/Messages.swift @@ -26,7 +26,7 @@ struct Messages: View { @FocusState private var focusedField: Field? @State var showDeleteMessageAlert = false - @State private var deleteMessageId: Int32 = 0 + @State private var deleteMessageId: Int64 = 0 public var broadcastNodeId: UInt32 = 4294967295 let timer = Timer.publish(every: 1, on: .main, in: .common).autoconnect() @@ -48,15 +48,16 @@ struct Messages: View { ForEach(messages) { message in HStack(alignment: .top) { - let currentUser: Bool = false//(bleManager.connectedNode != nil) && ((bleManager.connectedNode.id) == message.fromUser!.num) + let currentUser: Bool = false// message.fromUser != nil && ((bleManager.connectedPeripheral) == message.fromUser!.num) + - CircleText(text: "???", color: currentUser ? .accentColor : Color(.darkGray)).padding(.all, 5) + CircleText(text: (message.fromUser?.longName ?? "???"), color: currentUser ? .accentColor : Color(.darkGray)).padding(.all, 5) .gesture(LongPressGesture(minimumDuration: 2) .onEnded {_ in print("I want to delete message: \(message.messageId)") self.showDeleteMessageAlert = true self.deleteMessageId = message.messageId - + print(deleteMessageId) }) VStack(alignment: .leading) { @@ -193,16 +194,17 @@ struct Messages: View { } } .navigationTitle("Channel - Primary") - .navigationBarTitleDisplayMode(.inline) + //.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") ) + //ConnectedDevice(bluetoothOn: bleManager.isSwitchedOn, deviceConnected: bleManager.connectedPeripheral != nil, name: (bleManager.connectedNode != nil) ? bleManager.connectedNode.user.shortName : ((bleManager.connectedPeripheral != nil) ? bleManager.connectedPeripheral.name : "Unknown") ) }) - .onAppear { + .onAppear(perform: { + self.bleManager.context = context messageCount = messages.count - } + }) } } diff --git a/MeshtasticClient/Views/Nodes/NodeDetail.swift b/MeshtasticClient/Views/Nodes/NodeDetail.swift index e624d622..3563b7d0 100644 --- a/MeshtasticClient/Views/Nodes/NodeDetail.swift +++ b/MeshtasticClient/Views/Nodes/NodeDetail.swift @@ -1,176 +1,176 @@ -/* -Abstract: -A view showing the details for a node. -*/ - -import SwiftUI -import MapKit -import CoreLocation - -struct NodeDetail: View { - - @EnvironmentObject var bleManager: BLEManager - - var node: NodeInfoModel - - struct MapLocation: Identifiable { - let id = UUID() - let name: String - let coordinate: CLLocationCoordinate2D - } - - var body: some View { - - GeometryReader { bounds in - - VStack { - - if node.position.coordinate != nil { - - let nodeCoordinatePosition = CLLocationCoordinate2D(latitude: node.position.latitude!, longitude: node.position.longitude!) - - let regionBinding = Binding( - get: { - MKCoordinateRegion(center: nodeCoordinatePosition, span: MKCoordinateSpan(latitudeDelta: 0.005, longitudeDelta: 0.005)) - }, - set: { _ in } - ) - let annotations = [MapLocation(name: node.user.shortName, coordinate: node.position.coordinate!)] - - Map(coordinateRegion: regionBinding, showsUserLocation: true, userTrackingMode: .none, annotationItems: annotations) { location in - MapAnnotation( - coordinate: location.coordinate, - content: { - CircleText(text: node.user.shortName, color: .accentColor) - } - ) - }.frame(idealWidth: bounds.size.width, minHeight: bounds.size.height / 2) - } else { - Image(node.user.hwModel) - .resizable() - .aspectRatio(contentMode: .fit) - .frame(width: bounds.size.width, height: bounds.size.height / 2) - } - ScrollView { - - HStack { - - VStack(alignment: .center) { - Text("AKA").font(.title2).fixedSize() - CircleText(text: node.user.shortName, color: .accentColor) - .offset(y: 10) - } - .padding([.leading, .trailing, .bottom]) - Divider() - if node.snr != nil && node.snr! > 0 { - VStack(alignment: .center) { - - Image(systemName: "waveform.path") - .font(.title) - .foregroundColor(.accentColor) - .symbolRenderingMode(.hierarchical) - Text("SNR").font(.title2).fixedSize() - Text(String(node.snr ?? 0)) - .font(.title2) - .foregroundColor(.gray) - } - Divider() - } - VStack(alignment: .center) { - BatteryIcon(batteryLevel: node.position.batteryLevel, font: .title, color: .accentColor) - if node.position.batteryLevel != nil && node.position.batteryLevel! > 0 { - Text("Battery").font(.title2).fixedSize() - Text(String(node.position.batteryLevel!) + "%") - .font(.title2) - .foregroundColor(.gray) - .symbolRenderingMode(.hierarchical) - } else { - Text("Powered").font(.title2) - } - } - - }.padding(4) - Divider() - HStack { - - Image(node.user.hwModel) - .resizable() - .frame(width: 60, height: 60) - .cornerRadius(5) - - Text("Model: " + String(node.user.hwModel)) - .font(.title3) - } - .padding() - Divider() - - if node.lastHeard > 0 { - - HStack { - - Image(systemName: "clock").font(.title2).foregroundColor(.accentColor) - let lastHeard = Date(timeIntervalSince1970: TimeInterval(node.lastHeard)) - Text("Last Heard: \(lastHeard, style: .relative) ago").font(.title3) - }.padding() - Divider() - } - - if node.position.coordinate != nil { - HStack(alignment: /*@START_MENU_TOKEN@*/.center/*@END_MENU_TOKEN@*/, spacing: 14) { - Image(systemName: "mappin").font(.title).foregroundColor(.accentColor) - VStack(alignment: .leading) { - Text("Latitude").font(.headline) - Text(String(node.position.latitude ?? 0)).font(.caption).foregroundColor(.gray) - } - Divider() - VStack(alignment: .leading) { - Text("Longitude").font(.headline) - Text(String(node.position.longitude ?? 0)).font(.caption).foregroundColor(.gray) - } - Divider() - VStack(alignment: .leading) { - Text("Altitude").font(.headline) - Text(String(node.position.altitude ?? 0) + " m").font(.caption).foregroundColor(.gray) - } - }.padding() - Divider() - } - HStack(alignment: .center) { - VStack { - HStack { - Image(systemName: "person").font(.title3).foregroundColor(.accentColor) - Text("Unique Id:").font(.title3) - } - Text(node.user.id).font(.headline).foregroundColor(.gray) - } - Divider() - VStack { - HStack { - Image(systemName: "number").font(.title3).foregroundColor(.accentColor) - Text("Node Number:").font(.title3) - } - Text(String(node.num)).font(.headline).foregroundColor(.gray) - } - }.padding() - } - }.navigationTitle(node.user.longName) - .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") ) - }) - }.ignoresSafeArea(.all, edges: [.leading, .trailing]) - } -} - -struct NodeDetail_Previews: PreviewProvider { - static let bleManager = BLEManager() - - static var previews: some View { - Group { - NodeDetail(node: bleManager.meshData.nodes[0]) - NodeDetail(node: bleManager.meshData.nodes[1]) - } - } -} +///* +//Abstract: +//A view showing the details for a node. +//*/ +// +//import SwiftUI +//import MapKit +//import CoreLocation +// +//struct NodeDetail: View { +// +// @EnvironmentObject var bleManager: BLEManager +// +// var node: NodeInfoModel +// +// struct MapLocation: Identifiable { +// let id = UUID() +// let name: String +// let coordinate: CLLocationCoordinate2D +// } +// +// var body: some View { +// +// GeometryReader { bounds in +// +// VStack { +// +// if node.position.coordinate != nil { +// +// let nodeCoordinatePosition = CLLocationCoordinate2D(latitude: node.position.latitude!, longitude: node.position.longitude!) +// +// let regionBinding = Binding( +// get: { +// MKCoordinateRegion(center: nodeCoordinatePosition, span: MKCoordinateSpan(latitudeDelta: 0.005, longitudeDelta: 0.005)) +// }, +// set: { _ in } +// ) +// let annotations = [MapLocation(name: node.user.shortName, coordinate: node.position.coordinate!)] +// +// Map(coordinateRegion: regionBinding, showsUserLocation: true, userTrackingMode: .none, annotationItems: annotations) { location in +// MapAnnotation( +// coordinate: location.coordinate, +// content: { +// CircleText(text: node.user.shortName, color: .accentColor) +// } +// ) +// }.frame(idealWidth: bounds.size.width, minHeight: bounds.size.height / 2) +// } else { +// Image(node.user.hwModel) +// .resizable() +// .aspectRatio(contentMode: .fit) +// .frame(width: bounds.size.width, height: bounds.size.height / 2) +// } +// ScrollView { +// +// HStack { +// +// VStack(alignment: .center) { +// Text("AKA").font(.title2).fixedSize() +// CircleText(text: node.user.shortName, color: .accentColor) +// .offset(y: 10) +// } +// .padding([.leading, .trailing, .bottom]) +// Divider() +// if node.snr != nil && node.snr! > 0 { +// VStack(alignment: .center) { +// +// Image(systemName: "waveform.path") +// .font(.title) +// .foregroundColor(.accentColor) +// .symbolRenderingMode(.hierarchical) +// Text("SNR").font(.title2).fixedSize() +// Text(String(node.snr ?? 0)) +// .font(.title2) +// .foregroundColor(.gray) +// } +// Divider() +// } +// VStack(alignment: .center) { +// BatteryIcon(batteryLevel: node.position.batteryLevel, font: .title, color: .accentColor) +// if node.position.batteryLevel != nil && node.position.batteryLevel! > 0 { +// Text("Battery").font(.title2).fixedSize() +// Text(String(node.position.batteryLevel!) + "%") +// .font(.title2) +// .foregroundColor(.gray) +// .symbolRenderingMode(.hierarchical) +// } else { +// Text("Powered").font(.title2) +// } +// } +// +// }.padding(4) +// Divider() +// HStack { +// +// Image(node.user.hwModel) +// .resizable() +// .frame(width: 60, height: 60) +// .cornerRadius(5) +// +// Text("Model: " + String(node.user.hwModel)) +// .font(.title3) +// } +// .padding() +// Divider() +// +// if node.lastHeard > 0 { +// +// HStack { +// +// Image(systemName: "clock").font(.title2).foregroundColor(.accentColor) +// let lastHeard = Date(timeIntervalSince1970: TimeInterval(node.lastHeard)) +// Text("Last Heard: \(lastHeard, style: .relative) ago").font(.title3) +// }.padding() +// Divider() +// } +// +// if node.position.coordinate != nil { +// HStack(alignment: /*@START_MENU_TOKEN@*/.center/*@END_MENU_TOKEN@*/, spacing: 14) { +// Image(systemName: "mappin").font(.title).foregroundColor(.accentColor) +// VStack(alignment: .leading) { +// Text("Latitude").font(.headline) +// Text(String(node.position.latitude ?? 0)).font(.caption).foregroundColor(.gray) +// } +// Divider() +// VStack(alignment: .leading) { +// Text("Longitude").font(.headline) +// Text(String(node.position.longitude ?? 0)).font(.caption).foregroundColor(.gray) +// } +// Divider() +// VStack(alignment: .leading) { +// Text("Altitude").font(.headline) +// Text(String(node.position.altitude ?? 0) + " m").font(.caption).foregroundColor(.gray) +// } +// }.padding() +// Divider() +// } +// HStack(alignment: .center) { +// VStack { +// HStack { +// Image(systemName: "person").font(.title3).foregroundColor(.accentColor) +// Text("Unique Id:").font(.title3) +// } +// Text(node.user.id).font(.headline).foregroundColor(.gray) +// } +// Divider() +// VStack { +// HStack { +// Image(systemName: "number").font(.title3).foregroundColor(.accentColor) +// Text("Node Number:").font(.title3) +// } +// Text(String(node.num)).font(.headline).foregroundColor(.gray) +// } +// }.padding() +// } +// }.navigationTitle(node.user.longName) +// .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") ) +// }) +// }.ignoresSafeArea(.all, edges: [.leading, .trailing]) +// } +//} +// +//struct NodeDetail_Previews: PreviewProvider { +// static let bleManager = BLEManager() +// +// static var previews: some View { +// Group { +// NodeDetail(node: bleManager.meshData.nodes[0]) +// NodeDetail(node: bleManager.meshData.nodes[1]) +// } +// } +//} diff --git a/MeshtasticClient/Views/Nodes/NodeInfoEntityDetail.swift b/MeshtasticClient/Views/Nodes/NodeInfoEntityDetail.swift new file mode 100644 index 00000000..790517d5 --- /dev/null +++ b/MeshtasticClient/Views/Nodes/NodeInfoEntityDetail.swift @@ -0,0 +1,175 @@ +/* +Abstract: +A view showing the details for a node. +*/ + +import SwiftUI +import MapKit +import CoreLocation + +struct NodeInfoEntityDetail: View { + + @EnvironmentObject var bleManager: BLEManager + + var node: NodeInfoEntity + + struct MapLocation: Identifiable { + let id = UUID() + let name: String + let coordinate: CLLocationCoordinate2D + } + + var body: some View { + + GeometryReader { bounds in + + VStack { + + if node.positions!.count > 0 { + +// let nodeCoordinatePosition = CLLocationCoordinate2D(latitude: node.position.latitude!, longitude: node.position.longitude!) +// +// let regionBinding = Binding( +// get: { +// MKCoordinateRegion(center: nodeCoordinatePosition, span: MKCoordinateSpan(latitudeDelta: 0.005, longitudeDelta: 0.005)) +// }, +// set: { _ in } +// ) +// let annotations = [MapLocation(name: node.user.shortName, coordinate: node.position.coordinate!)] +// +// Map(coordinateRegion: regionBinding, showsUserLocation: true, userTrackingMode: .none, annotationItems: annotations) { location in +// MapAnnotation( +// coordinate: location.coordinate, +// content: { +// CircleText(text: node.user.shortName, color: .accentColor) +// } +// ) +// }.frame(idealWidth: bounds.size.width, minHeight: bounds.size.height / 2) + } else { + Image(node.user?.hwModel ?? "UNSET") + .resizable() + .aspectRatio(contentMode: .fit) + .frame(width: bounds.size.width, height: bounds.size.height / 2) + } + ScrollView { + + HStack { + + VStack(alignment: .center) { + Text("AKA").font(.title2).fixedSize() + CircleText(text: node.user?.shortName ?? "???", color: .accentColor) + .offset(y: 10) + } + .padding([.leading, .trailing, .bottom]) + Divider() + if node.snr > 0 { + VStack(alignment: .center) { + + Image(systemName: "waveform.path") + .font(.title) + .foregroundColor(.accentColor) + .symbolRenderingMode(.hierarchical) + Text("SNR").font(.title2).fixedSize() + Text(String(node.snr)) + .font(.title2) + .foregroundColor(.gray) + } + Divider() + } +// VStack(alignment: .center) { +// BatteryIcon(batteryLevel: node.position.batteryLevel, font: .title, color: .accentColor) +// if node.position.batteryLevel != nil && node.position.batteryLevel! > 0 { +// Text("Battery").font(.title2).fixedSize() +// Text(String(node.position.batteryLevel!) + "%") +// .font(.title2) +// .foregroundColor(.gray) +// .symbolRenderingMode(.hierarchical) +// } else { +// Text("Powered").font(.title2) +// } +// } + + }.padding(4) + Divider() + HStack { + + Image(node.user!.hwModel ?? "UNSET") + .resizable() + .frame(width: 60, height: 60) + .cornerRadius(5) + + Text("Model: " + String(node.user!.hwModel ?? "UNSET")) + .font(.title3) + } + .padding() + Divider() + + if node.lastHeard > 0 { + + HStack { + + Image(systemName: "clock").font(.title2).foregroundColor(.accentColor) + let lastHeard = Date(timeIntervalSince1970: TimeInterval(node.lastHeard)) + Text("Last Heard: \(lastHeard, style: .relative) ago").font(.title3) + }.padding() + Divider() + } + +// if node.position.coordinate != nil { +// HStack(alignment: /*@START_MENU_TOKEN@*/.center/*@END_MENU_TOKEN@*/, spacing: 14) { +// Image(systemName: "mappin").font(.title).foregroundColor(.accentColor) +// VStack(alignment: .leading) { +// Text("Latitude").font(.headline) +// Text(String(node.position.latitude ?? 0)).font(.caption).foregroundColor(.gray) +// } +// Divider() +// VStack(alignment: .leading) { +// Text("Longitude").font(.headline) +// Text(String(node.position.longitude ?? 0)).font(.caption).foregroundColor(.gray) +// } +// Divider() +// VStack(alignment: .leading) { +// Text("Altitude").font(.headline) +// Text(String(node.position.altitude ?? 0) + " m").font(.caption).foregroundColor(.gray) +// } +// }.padding() +// Divider() +// } + HStack(alignment: .center) { + VStack { + HStack { + Image(systemName: "person").font(.title3).foregroundColor(.accentColor) + Text("Unique Id:").font(.title3) + } + Text(node.user?.userId ?? "??????").font(.headline).foregroundColor(.gray) + } + Divider() + VStack { + HStack { + Image(systemName: "number").font(.title3).foregroundColor(.accentColor) + Text("Node Number:").font(.title3) + } + Text(String(node.num)).font(.headline).foregroundColor(.gray) + } + }.padding() + } + }.navigationTitle(node.user!.longName ?? "Unknown") + .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") ) + }) + }.ignoresSafeArea(.all, edges: [.leading, .trailing]) + } +} + +struct NodeInfoEntityDetail_Previews: PreviewProvider { + static let bleManager = BLEManager() + + static var previews: some View { + Group { + //NodeInfoEntityDetail(node: node) + } + } +} diff --git a/MeshtasticClient/Views/Nodes/NodeList.swift b/MeshtasticClient/Views/Nodes/NodeList.swift index f80a5ee2..3c4f928e 100644 --- a/MeshtasticClient/Views/Nodes/NodeList.swift +++ b/MeshtasticClient/Views/Nodes/NodeList.swift @@ -11,89 +11,64 @@ import SwiftUI struct NodeList: View { - - @EnvironmentObject var bleManager: BLEManager - + + // CoreData + @Environment(\.managedObjectContext) var context + @EnvironmentObject var bleManager: BLEManager + + @FetchRequest( + sortDescriptors: [NSSortDescriptor(keyPath: \NodeInfoEntity.lastHeard, ascending: false)], + animation: .default) + + private var nodes: FetchedResults + @State private var selection: String? - @State private var showLocationOnly = false - - var filteredDevices: [NodeInfoModel] { - bleManager.meshData.nodes.filter { node in - (!showLocationOnly || node.position.coordinate != nil) - } - } - var body: some View { NavigationView { List { - if bleManager.meshData.nodes.count == 0 { + if nodes.count == 0 { Text("Scan for Radios").font(.largeTitle) - // .listRowSeparator(.hidden) Text("No LoRa Mesh Nodes Found").font(.title2) - // .listRowSeparator(.hidden) Text("Go to the bluetooth section in the bottom right menu and click the Start Scanning button to scan for nearby radios and find your Meshtastic device. Make sure your device is powered on and near your phone or tablet.") .font(.body) - // .listRowSeparator(.hidden) Text("Once the device shows under Available Devices touch the device you want to connect to and it will pull node information over BLE and populate the node list and mesh map in the Meshtastic app.") - // .listRowSeparator(.hidden) Text("Views with bluetooth functionality will show an indicator in the upper right hand corner show if bluetooth is on, and if a device is connected.") // .listRowSeparator(.hidden) Spacer() // .listRowSeparator(.hidden) } else { - Toggle(isOn: $showLocationOnly) { - Text("Nodes with location only") - } - ForEach(filteredDevices.sorted(by: { $0.lastHeard > $1.lastHeard })) { node in + ForEach( nodes ) { node in + let index = nodes.firstIndex(where: { $0.id == node.id }) + NavigationLink(destination: NodeInfoEntityDetail(node: node), tag: String(index!), selection: $selection) { - let index = filteredDevices.sorted(by: { $0.lastHeard > $1.lastHeard }).firstIndex(where: { $0.id == node.id }) + if bleManager.connectedPeripheral != nil && bleManager.connectedPeripheral.myInfo != nil { - NavigationLink(destination: NodeDetail(node: node), tag: String(index!), selection: $selection) { - - if bleManager.connectedPeripheral != nil && bleManager.connectedPeripheral.myInfo != nil { - - let connected: Bool = (bleManager.connectedPeripheral.myInfo!.myNodeNum == node.id) - NodeRow(node: node, connected: connected) - } else { - NodeRow(node: node, connected: false) - } - - } - .swipeActions(edge: .trailing) { - Button(role: .destructive) { - let nodeIndex = bleManager.meshData.nodes.firstIndex(where: { $0.num == node.num }) - bleManager.meshData.nodes.remove(at: nodeIndex!) - bleManager.meshData.save() - } label: { - - Label("Delete from app", systemImage: "trash") + let connected: Bool = (bleManager.connectedPeripheral.myInfo!.myNodeNum == node.id) + NodeInfoEntityRow(node: node, connected: connected) + } else { + NodeInfoEntityRow(node: node, connected: false) } - } - } + + } + } } } .navigationTitle("All Nodes") - .onAppear( - perform: { - bleManager.meshData.load() - if UIDevice.current.userInterfaceIdiom == .pad { - if bleManager.meshData.nodes.count > 0 { - selection = "0" - } + .onAppear{ + + self.bleManager.context = context + + if UIDevice.current.userInterfaceIdiom == .pad { + if nodes.count > 0 { + selection = "0" } } - ) + } } - .ignoresSafeArea(.all, edges: [.leading, .trailing]) + //.ignoresSafeArea(.all, edges: [.leading, .trailing]) .navigationViewStyle(DoubleColumnNavigationViewStyle()) } } - -struct NodeList_Previews: PreviewProvider { - static var previews: some View { - NodeList() - } -} diff --git a/MeshtasticClient/Views/Nodes/NodeMap.swift b/MeshtasticClient/Views/Nodes/NodeMap.swift index c3007cae..bd5f9932 100644 --- a/MeshtasticClient/Views/Nodes/NodeMap.swift +++ b/MeshtasticClient/Views/Nodes/NodeMap.swift @@ -12,13 +12,23 @@ import CoreLocation struct NodeMap: View { + // CoreData + @Environment(\.managedObjectContext) var context @EnvironmentObject var bleManager: BLEManager + + @FetchRequest( + sortDescriptors: [NSSortDescriptor(keyPath: \NodeInfoEntity.lastHeard, ascending: false)], + animation: .default) + + private var locationNodes: FetchedResults - var locationNodes: [NodeInfoModel] { - bleManager.meshData.nodes.filter { node in - (node.position.coordinate != nil) - } - } + //var locationNodes: [NodeInfoModel]// { + //bleManager.meshData.nodes.filter { node in + // (node.position.coordinate != nil) + // } + //} + + struct MapLocation: Identifiable { let id = UUID() let name: String @@ -39,20 +49,20 @@ struct NodeMap: View { NavigationView { ZStack { - Map(coordinateRegion: regionBinding, - interactionModes: [.all], - showsUserLocation: true, - userTrackingMode: .constant(.follow), annotationItems: locationNodes) { location in - - MapAnnotation( - coordinate: location.position.coordinate!, - content: { - CircleText(text: location.user.shortName, color: .accentColor) - } - ) - } - .frame(maxHeight: .infinity) - .ignoresSafeArea(.all, edges: [.leading, .trailing]) +// Map(coordinateRegion: regionBinding, +// interactionModes: [.all], +// showsUserLocation: true, +// userTrackingMode: .constant(.follow), annotationItems: locationNodes) { location in +// +// MapAnnotation( +// coordinate: location.position.coordinate!, +// content: { +// CircleText(text: location.user.shortName, color: .accentColor) +// } +// ) +// } +// .frame(maxHeight: .infinity) +// .ignoresSafeArea(.all, edges: [.leading, .trailing]) } .navigationTitle("Mesh Map") .navigationBarTitleDisplayMode(.inline) diff --git a/MeshtasticClient/Views/Nodes/NodeRow.swift b/MeshtasticClient/Views/Nodes/NodeRow.swift index b5a66351..d9eb2fa0 100644 --- a/MeshtasticClient/Views/Nodes/NodeRow.swift +++ b/MeshtasticClient/Views/Nodes/NodeRow.swift @@ -1,64 +1,64 @@ -import SwiftUI - -struct NodeRow: View { - var node: NodeInfoModel - var connected: Bool - - var body: some View { - VStack(alignment: .leading) { - - HStack { - - CircleText(text: node.user.shortName, color: Color.accentColor).offset(y: 1).padding(.trailing, 5) - .offset(x: -15) - - if UIDevice.current.userInterfaceIdiom == .pad { - Text(node.user.longName).font(.headline) - .offset(x: -15) - } else { - Text(node.user.longName).font(.title) - .offset(x: -15) - } - } - .padding(.bottom, 10) - - HStack(alignment: .bottom) { - - Image(systemName: "clock.badge.checkmark.fill").font(.headline).foregroundColor(.accentColor).symbolRenderingMode(.hierarchical) - - if UIDevice.current.userInterfaceIdiom == .pad { - - if connected { - Text("Currently Connected").font(.caption).foregroundColor(Color.accentColor) - } else if node.lastHeard > 0 { - let lastHeard = Date(timeIntervalSince1970: TimeInterval(node.lastHeard)) - Text("Last Heard: \(lastHeard, style: .relative) ago").font(.caption).foregroundColor(.gray) - } else { - Text("Last Heard: Unknown").font(.caption).foregroundColor(.gray) - } - - } else { - if connected { - Text("Currently Connected").font(.subheadline).foregroundColor(Color.accentColor) - } else if node.lastHeard > 0 { - let lastHeard = Date(timeIntervalSince1970: TimeInterval(node.lastHeard)) - Text("Last Heard: \(lastHeard, style: .relative) ago").font(.subheadline).foregroundColor(.gray) - } else { - Text("Last Heard: Unknown").font(.subheadline).foregroundColor(.gray) - } - } - } - }.padding([.leading, .top, .bottom]) - } -} - -struct NodeRow_Previews: PreviewProvider { - static var nodes = BLEManager().meshData.nodes - - static var previews: some View { - Group { - NodeRow(node: nodes[0], connected: true) - } - .previewLayout(.fixed(width: 300, height: 70)) - } -} +//import SwiftUI +// +//struct NodeRow: View { +// var node: NodeInfoModel +// var connected: Bool +// +// var body: some View { +// VStack(alignment: .leading) { +// +// HStack { +// +// CircleText(text: node.user.shortName, color: Color.accentColor).offset(y: 1).padding(.trailing, 5) +// .offset(x: -15) +// +// if UIDevice.current.userInterfaceIdiom == .pad { +// Text(node.user.longName).font(.headline) +// .offset(x: -15) +// } else { +// Text(node.user.longName).font(.title) +// .offset(x: -15) +// } +// } +// .padding(.bottom, 10) +// +// HStack(alignment: .bottom) { +// +// Image(systemName: "clock.badge.checkmark.fill").font(.headline).foregroundColor(.accentColor).symbolRenderingMode(.hierarchical) +// +// if UIDevice.current.userInterfaceIdiom == .pad { +// +// if connected { +// Text("Currently Connected").font(.caption).foregroundColor(Color.accentColor) +// } else if node.lastHeard > 0 { +// let lastHeard = Date(timeIntervalSince1970: TimeInterval(node.lastHeard)) +// Text("Last Heard: \(lastHeard, style: .relative) ago").font(.caption).foregroundColor(.gray) +// } else { +// Text("Last Heard: Unknown").font(.caption).foregroundColor(.gray) +// } +// +// } else { +// if connected { +// Text("Currently Connected").font(.subheadline).foregroundColor(Color.accentColor) +// } else if node.lastHeard > 0 { +// let lastHeard = Date(timeIntervalSince1970: TimeInterval(node.lastHeard)) +// Text("Last Heard: \(lastHeard, style: .relative) ago").font(.subheadline).foregroundColor(.gray) +// } else { +// Text("Last Heard: Unknown").font(.subheadline).foregroundColor(.gray) +// } +// } +// } +// }.padding([.leading, .top, .bottom]) +// } +//} +// +//struct NodeRow_Previews: PreviewProvider { +// static var nodes = BLEManager().meshData.nodes +// +// static var previews: some View { +// Group { +// NodeRow(node: nodes[0], connected: true) +// } +// .previewLayout(.fixed(width: 300, height: 70)) +// } +//}