From 4a08431766e78cd949eab6484b76109e9aa0c24e Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Sun, 12 Dec 2021 17:17:46 -0800 Subject: [PATCH] CoreData commit 1 Messages replacement --- Meshtastic Client.xcodeproj/project.pbxproj | 18 + .../xcdebugger/Breakpoints_v2.xcbkptlist | 40 +- MeshtasticClient/Helpers/BLEManager.swift | 466 ++++++++++++------ MeshtasticClient/MeshtasticClientApp.swift | 7 +- .../Persistence/Persistence.swift | 88 ++-- .../Views/Bluetooth/Connect.swift | 9 +- .../Views/Messages/Messages.swift | 61 ++- .../Views/Settings/AppSettings.swift | 2 +- 8 files changed, 422 insertions(+), 269 deletions(-) diff --git a/Meshtastic Client.xcodeproj/project.pbxproj b/Meshtastic Client.xcodeproj/project.pbxproj index 8a2e26c8..929cd725 100644 --- a/Meshtastic Client.xcodeproj/project.pbxproj +++ b/Meshtastic Client.xcodeproj/project.pbxproj @@ -29,6 +29,7 @@ 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 */; }; + DD9D8F2F2764403B00080993 /* Meshtastic.xcdatamodeld in Sources */ = {isa = PBXBuildFile; fileRef = DD9D8F2D2764403B00080993 /* Meshtastic.xcdatamodeld */; }; DDAF8C5326EB1DF10058C060 /* BLEManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDAF8C5226EB1DF10058C060 /* BLEManager.swift */; }; DDAF8C5826ED07FD0058C060 /* mesh.pb.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDAF8C5726ED07FD0058C060 /* mesh.pb.swift */; }; DDAF8C5D26ED09490058C060 /* portnums.pb.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDAF8C5C26ED09490058C060 /* portnums.pb.swift */; }; @@ -92,6 +93,7 @@ DD90860B26F684AF00DC5189 /* BatteryIcon.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BatteryIcon.swift; sourceTree = ""; }; DD90860D26F69BAE00DC5189 /* NodeMap.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NodeMap.swift; sourceTree = ""; }; DD913638270DFF4C00D7ACF3 /* LocalNotificationManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocalNotificationManager.swift; sourceTree = ""; }; + DD9D8F2E2764403B00080993 /* CoreDataSample.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = CoreDataSample.xcdatamodel; sourceTree = ""; }; DDAF8C5226EB1DF10058C060 /* BLEManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BLEManager.swift; sourceTree = ""; }; DDAF8C5726ED07FD0058C060 /* mesh.pb.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = mesh.pb.swift; sourceTree = ""; }; DDAF8C5C26ED09490058C060 /* portnums.pb.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = portnums.pb.swift; sourceTree = ""; }; @@ -203,6 +205,7 @@ DDC2E14B26CE248E0042C5E4 = { isa = PBXGroup; children = ( + DD9D8F2D2764403B00080993 /* Meshtastic.xcdatamodeld */, DDC2E15626CE248E0042C5E4 /* MeshtasticClient */, DDC2E16D26CE248F0042C5E4 /* MeshtasticClientTests */, DDC2E17826CE248F0042C5E4 /* MeshtasticClientUITests */, @@ -517,6 +520,7 @@ DDAF8C6226ED0A230058C060 /* mqtt.pb.swift in Sources */, DDF924CA26FBB953009FE055 /* ConnectedDevice.swift in Sources */, DDAF8C5D26ED09490058C060 /* portnums.pb.swift in Sources */, + DD9D8F2F2764403B00080993 /* Meshtastic.xcdatamodeld in Sources */, DD47E3DF26F39D9F00029299 /* MyInfoModel.swift in Sources */, DD23A50F26FD1B4400D9B90C /* PeripheralModel.swift in Sources */, DD47E3CE26F103C600029299 /* NodeList.swift in Sources */, @@ -883,6 +887,20 @@ productName = SwiftProtobuf; }; /* End XCSwiftPackageProductDependency section */ + +/* Begin XCVersionGroup section */ + DD9D8F2D2764403B00080993 /* Meshtastic.xcdatamodeld */ = { + isa = XCVersionGroup; + children = ( + DD9D8F2E2764403B00080993 /* CoreDataSample.xcdatamodel */, + ); + currentVersion = DD9D8F2E2764403B00080993 /* CoreDataSample.xcdatamodel */; + name = Meshtastic.xcdatamodeld; + path = MeshtasticClient/Meshtastic.xcdatamodeld; + sourceTree = ""; + versionGroupType = wrapper.xcdatamodel; + }; +/* End XCVersionGroup section */ }; rootObject = DDC2E14C26CE248E0042C5E4 /* Project object */; } 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 55e733ce..4574a97c 100644 --- a/Meshtastic Client.xcodeproj/xcuserdata/garthvanderhouwen.xcuserdatad/xcdebugger/Breakpoints_v2.xcbkptlist +++ b/Meshtastic Client.xcodeproj/xcuserdata/garthvanderhouwen.xcuserdatad/xcdebugger/Breakpoints_v2.xcbkptlist @@ -32,38 +32,6 @@ stopOnStyle = "0"> - - - - - - - - diff --git a/MeshtasticClient/Helpers/BLEManager.swift b/MeshtasticClient/Helpers/BLEManager.swift index f2f03e80..f7fa7e82 100644 --- a/MeshtasticClient/Helpers/BLEManager.swift +++ b/MeshtasticClient/Helpers/BLEManager.swift @@ -8,25 +8,20 @@ import SwiftUI // --------------------------------------------------------------------------------------- class BLEManager: NSObject, ObservableObject, CBCentralManagerDelegate, CBPeripheralDelegate { + static let shared = BLEManager() + private static var documentsFolder: URL { do { - return try FileManager.default.url( - for: .documentDirectory, - in: .userDomainMask, - appropriateFor: nil, - create: true) + return try FileManager.default.url(for: .documentDirectory, in: .userDomainMask, appropriateFor: nil, create: true) } catch { fatalError("Can't find documents directory.") } } - - // Core Data - @Environment(\.managedObjectContext) private var viewContext - - @Published var meshData: MeshData - @Published var messageData: MessageData - - private var centralManager: CBCentralManager! + + var context: NSManagedObjectContext? + private var centralManager: CBCentralManager! + + @Published var peripherals = [Peripheral]() @Published var connectedPeripheral: Peripheral! @Published var connectedNode: NodeInfoModel! @@ -37,13 +32,9 @@ class BLEManager: NSObject, ObservableObject, CBCentralManagerDelegate, CBPeriph @Published var isScanning: Bool = false @Published var isConnected: Bool = false - @Published var peripherals = [Peripheral]() - var timeoutTimer: Timer? var timeoutTimerCount = 0 - private var meshLoggingEnabled: Bool = false - private var broadcastNodeId: UInt32 = 4294967295 /* Meshtastic Service Details */ @@ -55,24 +46,31 @@ 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") - + + private var meshLoggingEnabled: Bool = false let meshLog = documentsFolder.appendingPathComponent("meshlog.txt") - /* init BLEManager */ + + //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.messageData = MessageData() self.lastConnectedPeripheral = "" self.lastConnectionError = "" super.init() + //let bleQueue: DispatchQueue = DispatchQueue(label: "CentralManager") centralManager = CBCentralManager(delegate: self, queue: nil) meshData.load() - messageData.load() + //messageData.load() } - // called when bluetooth is enabled/disabled for the app + // MARK: Bluetooth enabled/disabled for the app func centralManagerDidUpdateState(_ central: CBCentralManager) { if central.state == .poweredOn { @@ -84,6 +82,8 @@ class BLEManager: NSObject, ObservableObject, CBCentralManagerDelegate, CBPeriph } } + + // MARK: Scanning for BLE Devices // Scan for nearby BLE devices using the Meshtastic BLE service ID func startScanning() { @@ -95,7 +95,7 @@ class BLEManager: NSObject, ObservableObject, CBCentralManagerDelegate, CBPeriph } } - // Stop Scanning For BLE Devices + // Stop Scanning For BLE Devices func stopScanning() { if centralManager.isScanning { @@ -106,6 +106,7 @@ class BLEManager: NSObject, ObservableObject, CBCentralManagerDelegate, CBPeriph } } + // MARK: BLE Connect functions /// The action after the timeout-timer has fired /// /// - Parameters: @@ -160,15 +161,14 @@ class BLEManager: NSObject, ObservableObject, CBCentralManagerDelegate, CBPeriph RunLoop.current.add(self.timeoutTimer!, forMode: .common) } - // Disconnect Peripheral function + // Disconnect Connected Peripheral func disconnectPeripheral() { guard let connectedPeripheral = connectedPeripheral else { return } self.centralManager?.cancelPeripheralConnection(connectedPeripheral.peripheral) - } - // Called each time a peripheral is discovered + //Called each time a peripheral is discovered func centralManager(_ central: CBCentralManager, didDiscover peripheral: CBPeripheral, advertisementData: [String: Any], rssi RSSI: NSNumber) { var peripheralName: String = peripheral.name ?? "Unknown" @@ -197,7 +197,7 @@ class BLEManager: NSObject, ObservableObject, CBCentralManagerDelegate, CBPeriph } } - // called when a peripheral is connected + // Called when a peripheral is connected func centralManager(_ central: CBCentralManager, didConnect peripheral: CBPeripheral) { // guard let connectedPeripheral = connectedPeripheral else { return } @@ -223,6 +223,7 @@ class BLEManager: NSObject, ObservableObject, CBCentralManagerDelegate, CBPeriph } + // Called when a Peripheral fails to connect func centralManager(_ central: CBCentralManager, didFailToConnect peripheral: CBPeripheral, error: Error?) { if meshLoggingEnabled { MeshLogger.log("BLE Failed to Connect: \(peripheral.name ?? "Unknown")") } @@ -282,7 +283,7 @@ class BLEManager: NSObject, ObservableObject, CBCentralManagerDelegate, CBPeriph } } - // Discover Services Event + //MARK: Peripheral Services functions func peripheral(_ peripheral: CBPeripheral, didDiscoverServices error: Error?) { if let e = error { @@ -303,7 +304,7 @@ class BLEManager: NSObject, ObservableObject, CBCentralManagerDelegate, CBPeriph } } - // Discover Characteristics Event + //MARK: Discover Characteristics Event func peripheral(_ peripheral: CBPeripheral, didDiscoverCharacteristicsFor service: CBService, error: Error?) { if let e = error { @@ -348,31 +349,15 @@ class BLEManager: NSObject, ObservableObject, CBCentralManagerDelegate, CBPeriph print("didUpdateNotificationStateFor char: \(characteristic.uuid.uuidString) \(characteristic.isNotifying)") if meshLoggingEnabled { MeshLogger.log("didUpdateNotificationStateFor char: \(characteristic.uuid.uuidString) \(characteristic.isNotifying)") } + if let errorText = error?.localizedDescription { print("didUpdateNotificationStateFor error: \(errorText)") - } - // commandLock.lock() - - // if let index = commandConditions.firstIndex(where: { (condition) -> Bool in - // if case .notificationStateUpdate(characteristic: characteristic, enabled: characteristic.isNotifying) = condition { - // return true - // } else { - // return false - // } - // }) { - // commandConditions.remove(at: index) - // commandError = error - - // if commandConditions.isEmpty { - // commandLock.broadcast() - // } - // } - - // commandLock.unlock() - // ?.peripheralManager(self, didUpdateNotificationStateFor: characteristic) + } } - // Data Read / Update Characteristic Event + //MARK: Data Read / Update Characteristic Event + //TODO: Convert to CoreData + //FIXME: Remove broken JSON file data layer implementation func peripheral(_ peripheral: CBPeripheral, didUpdateValueFor characteristic: CBCharacteristic, error: Error?) { if let e = error { @@ -383,10 +368,11 @@ class BLEManager: NSObject, ObservableObject, CBCentralManagerDelegate, CBPeriph switch characteristic.uuid { case FROMNUM_UUID: peripheral.readValue(for: FROMNUM_characteristic) - // let byteArrayFromData: [UInt8] = [UInt8](characteristic.value!) - // let stringFromByteArray = String(data: Data(_: byteArrayFromData), encoding: .utf8) - // print("string array data \(stringFromByteArray!)") - // print(characteristic.value?. ?? "no value") + let characteristicValue: [UInt8] = [UInt8](characteristic.value!) + let bigEndianUInt32 = characteristicValue.withUnsafeBytes { $0.load(as: UInt32.self) } + let returnValue = CFByteOrderGetCurrent() == CFByteOrder(CFByteOrderLittleEndian.rawValue) + ? UInt32(bigEndian: bigEndianUInt32) : bigEndianUInt32 + //print(returnValue) case FROMRADIO_UUID: if characteristic.value == nil || characteristic.value!.isEmpty { @@ -400,74 +386,220 @@ class BLEManager: NSObject, ObservableObject, CBCentralManagerDelegate, CBPeriph print("Print DecodedInfo") print(decodedInfo) + // MyInfo Data if decodedInfo.myInfo.myNodeNum != 0 { - - // Create a MyInfoModel - let myInfoModel = MyInfoModel( - myNodeNum: decodedInfo.myInfo.myNodeNum, - hasGps: decodedInfo.myInfo.hasGps_p, - numBands: decodedInfo.myInfo.numBands, - maxChannels: decodedInfo.myInfo.maxChannels, - firmwareVersion: decodedInfo.myInfo.firmwareVersion, - messageTimeoutMsec: decodedInfo.myInfo.messageTimeoutMsec, - minAppVersion: decodedInfo.myInfo.minAppVersion) - - // Save it to the connected nodeInfo - if connectedPeripheral != nil { - connectedPeripheral.myInfo = myInfoModel - // Save it to the connected node - connectedNode = meshData.nodes.first(where: {$0.num == myInfoModel.myNodeNum}) - - } - // Since the data is from the device itself we save all myInfo objects since they are always the most up to date - if connectedNode != nil { - - connectedNode.myInfo = myInfoModel - let nodeIndex = meshData.nodes.firstIndex(where: { $0.id == decodedInfo.myInfo.myNodeNum }) - // meshData.nodes.remove(at: nodeIndex!) - // meshData.nodes.append(connectedNode) - if nodeIndex != nil { - meshData.nodes[nodeIndex!] = connectedNode - meshData.save() + + print("Save a CoreData MyInfoEntity") + + 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 + if fetchedMyInfo.isEmpty { + let myInfo = MyInfoEntity(context: context!) + myInfo.myNodeNum = Int64(decodedInfo.myInfo.myNodeNum) + myInfo.hasGps = decodedInfo.myInfo.hasGps_p + myInfo.numBands = Int32(bitPattern: decodedInfo.myInfo.numBands) + myInfo.firmwareVersion = decodedInfo.myInfo.firmwareVersion + 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)") + } } - print("Saved a myInfo for \(decodedInfo.myInfo.myNodeNum)") - if meshLoggingEnabled { MeshLogger.log("BLE FROMRADIO received and myInfo saved for \(peripheral.name ?? "Unknown")") } + } catch { + } + + + + + //if meshLoggingEnabled { MeshLogger.log("BLE FROMRADIO received and myInfo saved for \(peripheral.name ?? String(myInfo.myNodeNum))") } } + // NodeInfo Data if decodedInfo.nodeInfo.num != 0 { + + print("Save a CoreData NodeInfoEntity") + + let fetchNodeRequest:NSFetchRequest = NSFetchRequest.init(entityName: "NodeInfoEntity") + fetchNodeRequest.predicate = NSPredicate(format: "num == %i", Int64(decodedInfo.nodeInfo.num)) + + do { + let fetchedNode = try context?.fetch(fetchNodeRequest) as! [NodeInfoEntity] + // Not Found Insert + if fetchedNode.isEmpty { + + let newNode = NodeInfoEntity(context: context!) + newNode.timestamp = Date() + newNode.id = Int64(decodedInfo.nodeInfo.num) + newNode.num = Int64(decodedInfo.nodeInfo.num) + newNode.lastHeard = Int32(bitPattern: decodedInfo.nodeInfo.lastHeard) + newNode.snr = decodedInfo.nodeInfo.snr + + let userIdLast4: String = String(decodedInfo.nodeInfo.user.id.suffix(4)) + newNode.bleName = "Meshtastic_" + userIdLast4 + + if decodedInfo.nodeInfo.hasUser { - print("Save a nodeInfo") - print(decodedInfo.nodeInfo) - 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() - + let newUser = UserEntity(context: context!) + newUser.userId = decodedInfo.nodeInfo.user.id + newUser.num = Int64(decodedInfo.nodeInfo.num) + newUser.longName = decodedInfo.nodeInfo.user.longName + newUser.shortName = decodedInfo.nodeInfo.user.shortName + newUser.macaddr = decodedInfo.nodeInfo.user.macaddr + newUser.hwModel = String(describing: decodedInfo.nodeInfo.user.hwModel).uppercased() + 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) + } + + // Look for a MyInfo + let fetchMyInfoRequest:NSFetchRequest = NSFetchRequest.init(entityName: "MyInfoEntity") + fetchMyInfoRequest.predicate = NSPredicate(format: "myNodeNum == %i", Int64(decodedInfo.nodeInfo.num)) + + do { + + let fetchedMyInfo = try context?.fetch(fetchMyInfoRequest) as! [MyInfoEntity] + if fetchedMyInfo.count > 0 { + newNode.myInfo = fetchedMyInfo[0] + + } + + } catch { + print("Fetch MyInfo Error") + } + } else { + + let updatedNode = fetchedNode[0] + updatedNode.timestamp = Date() + updatedNode.lastHeard = Int32(bitPattern: decodedInfo.nodeInfo.lastHeard) + updatedNode.snr = decodedInfo.nodeInfo.snr + + if decodedInfo.nodeInfo.hasUser { - // Data is older than what the app already has - return + 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 + } + } + // Look for a MyInfo + let fetchMyInfoRequest:NSFetchRequest = NSFetchRequest.init(entityName: "MyInfoEntity") + fetchMyInfoRequest.predicate = NSPredicate(format: "myNodeNum == %i", Int64(decodedInfo.nodeInfo.num)) + + do { + + let fetchedMyInfo = try context?.fetch(fetchMyInfoRequest) as! [MyInfoEntity] + if fetchedMyInfo.count > 0 { + + updatedNode.myInfo = fetchedMyInfo[0] + + } + + } catch { + print("Fetch MyInfo Error") + } } + do { + + try context!.save() + print("Saved a nodeInfo for \(decodedInfo.nodeInfo.num)") + + } catch { + + context!.rollback() + + let nsError = error as NSError + print("Error Saving CoreData NodeInfoEntity: \(nsError)") + } + + } catch { + + print("Fetch NodeInfoEntity Error") + } + + if decodedInfo.nodeInfo.hasUser { + + print("BLE FROMRADIO received and nodeInfo saved for \(decodedInfo.nodeInfo.user.longName)") + if meshLoggingEnabled { MeshLogger.log("BLE FROMRADIO received and nodeInfo saved for \(decodedInfo.nodeInfo.user.longName)") } + + } else { + + 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)") } } - // 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 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( @@ -493,11 +625,6 @@ class BLEManager: NSObject, ObservableObject, CBCentralManagerDelegate, CBPeriph ) meshData.save() if meshLoggingEnabled { MeshLogger.log("BLE FROMRADIO received and nodeInfo saved for \(decodedInfo.nodeInfo.user.longName)") } - - if connectedNode == nil { - - // connectedNode = meshData.nodes.first(where: {$0.num == connectedPeripheral.myInfo!.myNodeNum}) - } } } // Handle assorted app packets @@ -505,6 +632,8 @@ class BLEManager: NSObject, ObservableObject, CBCentralManagerDelegate, CBPeriph print("Handle a Packet") do { + + //!!!: Switch Messages Tab to coredata // Text Message App - Primary Broadcast Channel if decodedInfo.packet.decoded.portnum == PortNum.textMessageApp { @@ -512,50 +641,71 @@ class BLEManager: NSObject, ObservableObject, CBCentralManagerDelegate, CBPeriph print("Message Text: \(messageText)") if meshLoggingEnabled { MeshLogger.log("BLE FROMRADIO received for text message app \(messageText)") } + + let messageUsers:NSFetchRequest = NSFetchRequest.init(entityName: "UserEntity") + messageUsers.predicate = NSPredicate(format:"num IN %@", [decodedInfo.packet.to, decodedInfo.packet.from]) + + do { + + let fetchedUsers = try context?.fetch(messageUsers) as! [UserEntity] + + let newMessage = MessageEntity(context: context!) + newMessage.messageId = Int32(bitPattern: decodedInfo.packet.id) + newMessage.messageTimestamp = Int32(bitPattern: decodedInfo.packet.rxTime) + newMessage.receivedACK = false + newMessage.direction = "IN" + + if decodedInfo.packet.to == broadcastNodeId { + + 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 + + } else { + + newMessage.toUser = fetchedUsers.first(where: { $0.num == decodedInfo.packet.to }) + } + + newMessage.fromUser = fetchedUsers.first(where: { $0.num == decodedInfo.packet.from }) + newMessage.messagePayload = messageText + + do { + + try context!.save() + print("Saved a new message for \(decodedInfo.packet.id)") + + // Create an iOS Notification for the received message and schedule it immediately + let manager = LocalNotificationManager() - let fromUser = meshData.nodes.first(where: { $0.id == decodedInfo.packet.from }) - - var toUserLongName: String = "Broadcast" - var toUserShortName: String = "BC" - - if decodedInfo.packet.to != broadcastNodeId { - - let toUser = meshData.nodes.first(where: { $0.id == decodedInfo.packet.from }) - toUserLongName = toUser?.user.longName ?? "Unknown" - toUserShortName = toUser?.user.shortName ?? "???" + manager.notifications = [ + Notification( + id: ("notification.id.\(decodedInfo.packet.id)"), + title: "\(newMessage.fromUser?.longName ?? "Unknown")", + subtitle: "AKA \(newMessage.fromUser?.shortName ?? "???")", + content: messageText) + ] + manager.schedule() + if meshLoggingEnabled { MeshLogger.log("iOS Notification Scheduled for text message from \(newMessage.fromUser?.longName ?? "Unknown") \(messageText)") } + + } catch { + + print("Something went wrong: \(error)") + context!.rollback() + + let nsError = error as NSError + print("Unresolved error \(nsError)") + } + + } catch { + + print("Fetch Message To and From Users Error") } - - // Add the received message to the local messages list / file and save - messageData.messages.append( - MessageModel( - messageId: decodedInfo.packet.id, - messageTimeStamp: decodedInfo.packet.rxTime, - fromUserId: decodedInfo.packet.from, - toUserId: decodedInfo.packet.to, - fromUserLongName: fromUser?.user.longName ?? "Unknown", - toUserLongName: toUserLongName, - fromUserShortName: fromUser?.user.shortName ?? "???", - toUserShortName: toUserShortName, - receivedACK: decodedInfo.packet.decoded.wantResponse, - messagePayload: messageText, - direction: "IN") - ) - messageData.save() - - // Create an iOS Notification for the received message and schedule it immediately - let manager = LocalNotificationManager() - - manager.notifications = [ - Notification( - id: ("notification.id.\(decodedInfo.packet.id)"), - title: "\(fromUser?.user.longName ?? "Unknown")", - subtitle: "AKA \(fromUser?.user.shortName ?? "???")", - content: messageText) - ] - manager.schedule() - if meshLoggingEnabled { MeshLogger.log("iOS Notification Scheduled for text message from \(fromUser?.user.longName ?? "Unknown") \(messageText)") } } - } else if decodedInfo.packet.decoded.portnum == PortNum.nodeinfoApp { + } else if decodedInfo.packet.decoded.portnum == PortNum.nodeinfoApp { var updatedNode = meshData.nodes.first(where: {$0.id == decodedInfo.packet.from }) if updatedNode != nil { @@ -684,8 +834,8 @@ 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() + //messageData.messages.append(messageModel) + //messageData.save() success = true } } diff --git a/MeshtasticClient/MeshtasticClientApp.swift b/MeshtasticClient/MeshtasticClientApp.swift index e4e6e6b1..0d63f601 100644 --- a/MeshtasticClient/MeshtasticClientApp.swift +++ b/MeshtasticClient/MeshtasticClientApp.swift @@ -3,8 +3,10 @@ import CoreData @main struct MeshtasticClientApp: App { - - @ObservedObject private var bleManager: BLEManager = BLEManager() + + let persistenceController = PersistenceController.shared + + @ObservedObject private var bleManager: BLEManager = BLEManager.shared @ObservedObject private var userSettings: UserSettings = UserSettings() @Environment(\.scenePhase) var scenePhase @@ -12,6 +14,7 @@ struct MeshtasticClientApp: App { var body: some Scene { WindowGroup { ContentView() + .environment(\.managedObjectContext, persistenceController.container.viewContext) .environmentObject(userSettings) .environmentObject(bleManager) } diff --git a/MeshtasticClient/Persistence/Persistence.swift b/MeshtasticClient/Persistence/Persistence.swift index c80dcb9d..949691c1 100644 --- a/MeshtasticClient/Persistence/Persistence.swift +++ b/MeshtasticClient/Persistence/Persistence.swift @@ -6,52 +6,54 @@ // import CoreData -/* -struct PersistenceController { - static let shared = PersistenceController() +class PersistenceController { + + static let shared = PersistenceController() - static var preview: PersistenceController = { - let result = PersistenceController(inMemory: false) - let viewContext = result.container.viewContext - for _ in 0..<10 { - let newItem = NodeInfoEntity(context: viewContext) - newItem.timestamp = Date() - } - do { - try viewContext.save() - } catch { - // Replace this implementation with code to handle the error appropriately. - // fatalError() causes the application to generate a crash log and terminate. You should not use this function in a shipping application, although it may be useful during development. - let nsError = error as NSError - fatalError("Unresolved error \(nsError), \(nsError.userInfo)") - } - return result - }() + static var preview: PersistenceController = { + let result = PersistenceController(inMemory: false) + let viewContext = result.container.viewContext + for _ in 0..<10 { + let newItem = NodeInfoEntity(context: viewContext) + newItem.timestamp = Date() + } + do { + try viewContext.save() + } catch { + // Replace this implementation with code to handle the error appropriately. + // fatalError() causes the application to generate a crash log and terminate. You should not use this function in a shipping application, although it may be useful during development. + let nsError = error as NSError + fatalError("Unresolved error \(nsError), \(nsError.userInfo)") + } + return result + }() - let container: NSPersistentContainer + let container: NSPersistentContainer - init(inMemory: Bool = false) { - container = NSPersistentContainer(name: "Meshtastic") - if inMemory { - container.persistentStoreDescriptions.first!.url = URL(fileURLWithPath: "/dev/null") - } - container.loadPersistentStores(completionHandler: { (storeDescription, error) in - if let error = error as NSError? { - // Replace this implementation with code to handle the error appropriately. - // fatalError() causes the application to generate a crash log and terminate. You should not use this function in a shipping application, although it may be useful during development. + init(inMemory: Bool = false) { + container = NSPersistentContainer(name: "Meshtastic") + if inMemory { + container.persistentStoreDescriptions.first!.url = URL(fileURLWithPath: "/dev/null") + } + container.loadPersistentStores(completionHandler: { (storeDescription, error) in + // Merge policy that favors in memory data over data in the db + self.container.viewContext.mergePolicy = NSMergeByPropertyObjectTrumpMergePolicy + + if let error = error as NSError? { + // Replace this implementation with code to handle the error appropriately. + // fatalError() causes the application to generate a crash log and terminate. You should not use this function in a shipping application, although it may be useful during development. - /* - Typical reasons for an error here include: - * The parent directory does not exist, cannot be created, or disallows writing. - * The persistent store is not accessible, due to permissions or data protection when the device is locked. - * The device is out of space. - * The store could not be migrated to the current model version. - Check the error message to determine what the actual problem was. - */ - fatalError("Unresolved error \(error), \(error.userInfo)") - } - }) - } + /* + Typical reasons for an error here include: + * The parent directory does not exist, cannot be created, or disallows writing. + * The persistent store is not accessible, due to permissions or data protection when the device is locked. + * The device is out of space. + * The store could not be migrated to the current model version. + Check the error message to determine what the actual problem was. + */ + fatalError("Unresolved error \(error), \(error.userInfo)") + } + }) + } } -*/ diff --git a/MeshtasticClient/Views/Bluetooth/Connect.swift b/MeshtasticClient/Views/Bluetooth/Connect.swift index cc3d11d0..ba945f63 100644 --- a/MeshtasticClient/Views/Bluetooth/Connect.swift +++ b/MeshtasticClient/Views/Bluetooth/Connect.swift @@ -15,7 +15,8 @@ import CoreBluetooth struct Connect: View { - @EnvironmentObject var bleManager: BLEManager + @Environment(\.managedObjectContext) var context + @EnvironmentObject var bleManager: BLEManager @EnvironmentObject var userSettings: UserSettings @State var isPreferredRadio: Bool = false @@ -60,8 +61,8 @@ struct Connect: View { 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.myInfo != nil { - Text("FW Version: ").font(.caption)+Text(bleManager.connectedPeripheral.myInfo?.firmwareVersion ?? "(null)").font(.caption).foregroundColor(Color.gray) + if bleManager.connectedPeripheral != nil { + //Text("FW Version: ").font(.caption)+Text(bleManager.connectedPeripheral.myInfo?.firmwareVersion ?? "(null)").font(.caption).foregroundColor(Color.gray) } if bleManager.connectedPeripheral.subscribed { Text("Properly Subscribed").font(.caption) @@ -207,6 +208,8 @@ struct Connect: View { .navigationViewStyle(StackNavigationViewStyle()) .onAppear(perform: { + self.bleManager.context = context + if bleManager.connectedPeripheral != nil && userSettings.preferredPeripheralId == bleManager.connectedPeripheral.peripheral.identifier.uuidString { isPreferredRadio = true } else { diff --git a/MeshtasticClient/Views/Messages/Messages.swift b/MeshtasticClient/Views/Messages/Messages.swift index 21a674e2..3a62710d 100644 --- a/MeshtasticClient/Views/Messages/Messages.swift +++ b/MeshtasticClient/Views/Messages/Messages.swift @@ -8,6 +8,15 @@ struct Messages: View { enum Field: Hashable { case messageText } + + // CoreData + @Environment(\.managedObjectContext) var context + @EnvironmentObject var bleManager: BLEManager + + @FetchRequest( + sortDescriptors: [NSSortDescriptor(keyPath: \MessageEntity.messageTimestamp, ascending: true)], + animation: .default) + private var messages: FetchedResults // Keyboard State @State var typingMessage: String = "" @@ -16,14 +25,8 @@ struct Messages: View { @State private var lastTypingMessage = "" @FocusState private var focusedField: Field? - @Namespace var topId - @Namespace var bottomId - @State var showDeleteMessageAlert = false - @State private var deleteMessageId: UInt32 = 0 - - // Message Data and Bluetooth - @EnvironmentObject var bleManager: BLEManager + @State private var deleteMessageId: Int32 = 0 public var broadcastNodeId: UInt32 = 4294967295 let timer = Timer.publish(every: 1, on: .main, in: .common).autoconnect() @@ -38,18 +41,16 @@ struct Messages: View { ScrollViewReader { scrollView in - if self.bleManager.messageData.messages.count > 0 { + if self.messages.count > 0 { ScrollView { - Text("Hidden Top Anchor").hidden().frame(height: 0).id(topId) - - ForEach(bleManager.messageData.messages.sorted(by: { $0.messageTimestamp < $1.messageTimestamp })) { message in + ForEach(messages) { message in HStack(alignment: .top) { - let currentUser: Bool = (bleManager.connectedNode != nil) && ((bleManager.connectedNode.id) == message.fromUserId) + let currentUser: Bool = false//(bleManager.connectedNode != nil) && ((bleManager.connectedNode.id) == message.fromUser!.num) - CircleText(text: message.fromUserShortName, color: currentUser ? .accentColor : Color(.darkGray)).padding(.all, 5) + CircleText(text: "???", color: currentUser ? .accentColor : Color(.darkGray)).padding(.all, 5) .gesture(LongPressGesture(minimumDuration: 2) .onEnded {_ in print("I want to delete message: \(message.messageId)") @@ -59,7 +60,7 @@ struct Messages: View { }) VStack(alignment: .leading) { - Text(message.messagePayload) + Text(message.messagePayload ?? "EMPTY MESSAGE") .textSelection(.enabled) .padding(10) .foregroundColor(.white) @@ -87,11 +88,12 @@ struct Messages: View { print("OK button tapped") if deleteMessageId > 0 { - let messageIndex = bleManager.messageData.messages.firstIndex(where: { $0.messageId == deleteMessageId }) - bleManager.messageData.messages.remove(at: messageIndex!) - bleManager.messageData.save() - print("Deleted message: \(message.messageId)") - showDeleteMessageAlert = false + //let message = messages.first.where: { $0.messageId == deleteMessageId }) + //context.delete(object: message) + //bleManager.messageData.messages.remove(at: messageIndex!) + //bleManager.messageData.save() + //print("Deleted message: \(message.messageId)") + //showDeleteMessageAlert = false deleteMessageId = 0 } }, @@ -99,16 +101,23 @@ struct Messages: View { ) } } - .onAppear(perform: { scrollView.scrollTo(bottomId) }) - Text("Hidden Bottom Anchor").hidden().frame(height: 0).id(bottomId) + .onAppear(perform: { + + self.bleManager.context = context + messageCount = messages.count + if messageCount > 0 { + scrollView.scrollTo(messages[messageCount-1].id, anchor: .bottom) + } + + }) } .onReceive(timer) { _ in - if messageCount < bleManager.messageData.messages.count { + if messageCount < messages.count { + + scrollView.scrollTo(messages[messageCount].id, anchor: .bottom) + messageCount = messages.count - bleManager.messageData.load() - scrollView.scrollTo(bottomId) - messageCount = bleManager.messageData.messages.count } } .padding(.horizontal) @@ -193,7 +202,7 @@ struct Messages: View { }) .onAppear { - messageCount = bleManager.messageData.messages.count + messageCount = messages.count } } } diff --git a/MeshtasticClient/Views/Settings/AppSettings.swift b/MeshtasticClient/Views/Settings/AppSettings.swift index 603193cb..39428d76 100644 --- a/MeshtasticClient/Views/Settings/AppSettings.swift +++ b/MeshtasticClient/Views/Settings/AppSettings.swift @@ -68,7 +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 ?? true + self.meshActivityLog = UserDefaults.standard.object(forKey: "meshActivityLog") as? Bool ?? false } }