diff --git a/Meshtastic Client.xcodeproj/project.pbxproj b/Meshtastic Client.xcodeproj/project.pbxproj index d93f6c14..8a302fa9 100644 --- a/Meshtastic Client.xcodeproj/project.pbxproj +++ b/Meshtastic Client.xcodeproj/project.pbxproj @@ -38,6 +38,7 @@ DD913639270DFF4C00D7ACF3 /* LocalNotificationManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD913638270DFF4C00D7ACF3 /* LocalNotificationManager.swift */; }; DD9D8F2F2764403B00080993 /* Meshtastic.xcdatamodeld in Sources */ = {isa = PBXBuildFile; fileRef = DD9D8F2D2764403B00080993 /* Meshtastic.xcdatamodeld */; }; DDA6B2E928419CF2003E8C16 /* MeshPackets.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDA6B2E828419CF2003E8C16 /* MeshPackets.swift */; }; + DDA6B2EB28420A7B003E8C16 /* NodeAnnotation.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDA6B2EA28420A7B003E8C16 /* NodeAnnotation.swift */; }; 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 */; }; @@ -57,7 +58,9 @@ DDC2E1A726CEB3400042C5E4 /* LocationHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDC2E1A626CEB3400042C5E4 /* LocationHelper.swift */; }; DDC3B274283F411B00AC321C /* LastHeardText.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDC3B273283F411B00AC321C /* LastHeardText.swift */; }; DDC4D568275499A500A4208E /* Persistence.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDC4D567275499A500A4208E /* Persistence.swift */; }; - DDCA31322826009C00207175 /* PassKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = DDCA31312826009C00207175 /* PassKit.framework */; }; + DDD94A502845C8F5004A87A0 /* DateTimeText.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDD94A4F2845C8F5004A87A0 /* DateTimeText.swift */; }; + DDD9E4E4284B208E003777C5 /* UserEntityExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDD9E4E3284B208E003777C5 /* UserEntityExtension.swift */; }; + DDE393BA284E535700473991 /* ChannelHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDE393B9284E535700473991 /* ChannelHelper.swift */; }; DDF924CA26FBB953009FE055 /* ConnectedDevice.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDF924C926FBB953009FE055 /* ConnectedDevice.swift */; }; /* End PBXBuildFile section */ @@ -110,6 +113,7 @@ DD913638270DFF4C00D7ACF3 /* LocalNotificationManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocalNotificationManager.swift; sourceTree = ""; }; DD9D8F2E2764403B00080993 /* CoreDataSample.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = CoreDataSample.xcdatamodel; sourceTree = ""; }; DDA6B2E828419CF2003E8C16 /* MeshPackets.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MeshPackets.swift; sourceTree = ""; }; + DDA6B2EA28420A7B003E8C16 /* NodeAnnotation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NodeAnnotation.swift; 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 = ""; }; @@ -137,6 +141,9 @@ DDC3B273283F411B00AC321C /* LastHeardText.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LastHeardText.swift; sourceTree = ""; }; DDC4D567275499A500A4208E /* Persistence.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Persistence.swift; sourceTree = ""; }; DDCA31312826009C00207175 /* PassKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = PassKit.framework; path = System/Library/Frameworks/PassKit.framework; sourceTree = SDKROOT; }; + DDD94A4F2845C8F5004A87A0 /* DateTimeText.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DateTimeText.swift; sourceTree = ""; }; + DDD9E4E3284B208E003777C5 /* UserEntityExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserEntityExtension.swift; sourceTree = ""; }; + DDE393B9284E535700473991 /* ChannelHelper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChannelHelper.swift; sourceTree = ""; }; DDF924C926FBB953009FE055 /* ConnectedDevice.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConnectedDevice.swift; sourceTree = ""; }; /* End PBXFileReference section */ @@ -145,7 +152,6 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( - DDCA31322826009C00207175 /* PassKit.framework in Frameworks */, C9697FA527933B8C00250207 /* SQLite in Frameworks */, DD5394FC276993AD00AD86B1 /* SwiftProtobuf in Frameworks */, ); @@ -358,6 +364,8 @@ DD90860B26F684AF00DC5189 /* BatteryIcon.swift */, DDF924C926FBB953009FE055 /* ConnectedDevice.swift */, DDC3B273283F411B00AC321C /* LastHeardText.swift */, + DDA6B2EA28420A7B003E8C16 /* NodeAnnotation.swift */, + DDD94A4F2845C8F5004A87A0 /* DateTimeText.swift */, ); path = Helpers; sourceTree = ""; @@ -371,6 +379,7 @@ DD913638270DFF4C00D7ACF3 /* LocalNotificationManager.swift */, DD8169F8271F1A6100F4AB02 /* MeshLogger.swift */, DDA6B2E828419CF2003E8C16 /* MeshPackets.swift */, + DDE393B9284E535700473991 /* ChannelHelper.swift */, ); path = Helpers; sourceTree = ""; @@ -380,6 +389,7 @@ children = ( DDC4D567275499A500A4208E /* Persistence.swift */, DD5394FD276BA0EF00AD86B1 /* PositionEntityExtension.swift */, + DDD9E4E3284B208E003777C5 /* UserEntityExtension.swift */, ); path = Persistence; sourceTree = ""; @@ -456,7 +466,7 @@ TargetAttributes = { DDC2E15326CE248E0042C5E4 = { CreatedOnToolsVersion = 12.5.1; - LastSwiftMigration = 1320; + LastSwiftMigration = 1340; }; DDC2E16926CE248F0042C5E4 = { CreatedOnToolsVersion = 12.5.1; @@ -546,6 +556,7 @@ DD4DED9027AD2975004BA27E /* cannedmessages.pb.swift in Sources */, DD836AE726F6B38600ABCC23 /* Connect.swift in Sources */, DDAF8C6E26ED19040058C060 /* Extensions.swift in Sources */, + DDA6B2EB28420A7B003E8C16 /* NodeAnnotation.swift in Sources */, DDC2E1A726CEB3400042C5E4 /* LocationHelper.swift in Sources */, DD5394FE276BA0EF00AD86B1 /* PositionEntityExtension.swift in Sources */, DD913639270DFF4C00D7ACF3 /* LocalNotificationManager.swift in Sources */, @@ -571,6 +582,7 @@ DD47E3D626F17ED900029299 /* CircleText.swift in Sources */, DDC2E18F26CE25FE0042C5E4 /* ContentView.swift in Sources */, DD17E5DE277D49D400010EC2 /* storeforward.pb.swift in Sources */, + DDD9E4E4284B208E003777C5 /* UserEntityExtension.swift in Sources */, C9A88B55278B503C00BD810A /* MapViewModule.swift in Sources */, DDAF8C6326ED0A230058C060 /* admin.pb.swift in Sources */, C9483F6D2773017500998F6B /* MapView.swift in Sources */, @@ -579,6 +591,8 @@ DDC3B274283F411B00AC321C /* LastHeardText.swift in Sources */, DD2E65262767A01F00E45FC5 /* NodeDetail.swift in Sources */, DDA6B2E928419CF2003E8C16 /* MeshPackets.swift in Sources */, + DDD94A502845C8F5004A87A0 /* DateTimeText.swift in Sources */, + DDE393BA284E535700473991 /* ChannelHelper.swift in Sources */, C9A88B57278B559900BD810A /* apponly.pb.swift in Sources */, DD4C158E2824AA7E0032668E /* config.pb.swift in Sources */, DD47E3D926F3093800029299 /* MessageBubble.swift in Sources */, @@ -751,7 +765,7 @@ CODE_SIGN_ENTITLEMENTS = MeshtasticClient/MeshtasticClient.entitlements; "CODE_SIGN_IDENTITY[sdk=macosx*]" = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 7; + CURRENT_PROJECT_VERSION = 11; DEVELOPMENT_ASSET_PATHS = "\"MeshtasticClient/Preview Content\""; DEVELOPMENT_TEAM = GCH7VS5Y9R; ENABLE_PREVIEWS = YES; @@ -783,7 +797,7 @@ CODE_SIGN_ENTITLEMENTS = MeshtasticClient/MeshtasticClient.entitlements; "CODE_SIGN_IDENTITY[sdk=macosx*]" = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 7; + CURRENT_PROJECT_VERSION = 11; DEVELOPMENT_ASSET_PATHS = "\"MeshtasticClient/Preview Content\""; DEVELOPMENT_TEAM = GCH7VS5Y9R; ENABLE_PREVIEWS = YES; diff --git a/MeshtasticClient/Helpers/BLEManager.swift b/MeshtasticClient/Helpers/BLEManager.swift index b4e1fd2e..ce78febd 100644 --- a/MeshtasticClient/Helpers/BLEManager.swift +++ b/MeshtasticClient/Helpers/BLEManager.swift @@ -194,9 +194,9 @@ class BLEManager: NSObject, ObservableObject, CBCentralManagerDelegate, CBPeriph } } - let today = Date() - let oneMinuteAgo = Calendar.current.date(byAdding: .minute, value: -1, to: today)! - peripherals.removeAll(where: { $0.lastUpdate <= oneMinuteAgo}) + // let today = Date() + // let visibleDuration = Calendar.current.date(byAdding: .second, value: -7, to: Date())! + // peripherals.removeAll(where: { $0.lastUpdate <= visibleDuration}) } // Called when a peripheral is connected @@ -296,9 +296,15 @@ class BLEManager: NSObject, ObservableObject, CBCentralManagerDelegate, CBPeriph } } } + + func peripheral(_ peripheral: CBPeripheral, didModifyServices invalidatedServices: [CBService]) { + + print(invalidatedServices) + } // MARK: Discover Characteristics Event func peripheral(_ peripheral: CBPeripheral, didDiscoverCharacteristicsFor service: CBService, error: Error?) { + if let e = error { if meshLoggingEnabled { MeshLogger.log("🚫 BLE didDiscoverCharacteristicsFor error by \(peripheral.name ?? "Unknown") \(e)") } @@ -333,8 +339,7 @@ class BLEManager: NSObject, ObservableObject, CBCentralManagerDelegate, CBPeriph default: break } - - } + } } func peripheral(_ peripheral: CBPeripheral, didUpdateNotificationStateFor characteristic: CBCharacteristic, error: Error?) { @@ -386,15 +391,51 @@ class BLEManager: NSObject, ObservableObject, CBCentralManagerDelegate, CBPeriph switch decodedInfo.packet.decoded.portnum { case .unknownApp: - print("MyInfo or NodeInfo") + if decodedInfo.myInfo.myNodeNum != 0 { + + let myInfo = myInfoPacket(myInfo: decodedInfo.myInfo, meshLogging: meshLoggingEnabled, context: context!) + + if myInfo != nil { + + self.connectedPeripheral.bitrate = myInfo!.bitrate + self.connectedPeripheral.num = myInfo!.myNodeNum + lastConnnectionVersion = myInfo?.firmwareVersion ?? myInfo!.firmwareVersion ?? "Unknown" + self.connectedPeripheral.firmwareVersion = myInfo!.firmwareVersion ?? "Unknown" + self.connectedPeripheral.name = myInfo!.bleName ?? "Unknown" + } + + } else if decodedInfo.nodeInfo.num != 0 { + + let nodeInfo = nodeInfoPacket(nodeInfo: decodedInfo.nodeInfo, meshLogging: meshLoggingEnabled, context: context!) + + if nodeInfo != nil { + + self.connectedPeripheral.channelUtilization = decodedInfo.nodeInfo.deviceMetrics.channelUtilization + self.connectedPeripheral.airTime = decodedInfo.nodeInfo.deviceMetrics.airUtilTx + + if self.connectedPeripheral != nil && self.connectedPeripheral.num == nodeInfo!.num { + + if nodeInfo!.user != nil { + + connectedPeripheral.name = nodeInfo!.user!.longName ?? "Unknown" + } + } + } + + } else { + + if meshLoggingEnabled { MeshLogger.log("ℹ️ MESH PACKET received for Unknown App UNHANDLED \(try! decodedInfo.packet.jsonString())") } + } case .textMessageApp: textMessageAppPacket(packet: decodedInfo.packet, connectedNode: (self.connectedPeripheral != nil ? connectedPeripheral.num : 0), meshLogging: meshLoggingEnabled, context: context!) case .remoteHardwareApp: if meshLoggingEnabled { MeshLogger.log("ℹ️ MESH PACKET received for Remote Hardware App UNHANDLED \(try! decodedInfo.packet.jsonString())") } case .positionApp: positionPacket(packet: decodedInfo.packet, meshLogging: meshLoggingEnabled, context: context!) + case .waypointApp: + if meshLoggingEnabled { MeshLogger.log("ℹ️ MESH PACKET received for Waypoint App UNHANDLED \(try! decodedInfo.packet.jsonString())") } case .nodeinfoApp: - nodeInfoPacket(packet: decodedInfo.packet, meshLogging: meshLoggingEnabled, context: context!) + nodeInfoAppPacket(packet: decodedInfo.packet, meshLogging: meshLoggingEnabled, context: context!) case .routingApp: routingPacket(packet: decodedInfo.packet, meshLogging: meshLoggingEnabled, context: context!) case .adminApp: @@ -425,288 +466,40 @@ class BLEManager: NSObject, ObservableObject, CBCentralManagerDelegate, CBPeriph print("MAX PORT NUM OF 511") } - // MARK: Incoming MyInfo Packet - if decodedInfo.myInfo.myNodeNum != 0 { - - let fetchMyInfoRequest: NSFetchRequest = NSFetchRequest.init(entityName: "MyInfoEntity") - fetchMyInfoRequest.predicate = NSPredicate(format: "myNodeNum == %lld", 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.bitrate = decodedInfo.myInfo.bitrate - self.connectedPeripheral.bitrate = myInfo.bitrate - - // Swift does strings weird, this does work to get the version without the github hash - let lastDotIndex = decodedInfo.myInfo.firmwareVersion.lastIndex(of: ".") - var version = decodedInfo.myInfo.firmwareVersion[...(lastDotIndex ?? String.Index(utf16Offset: 6, in: decodedInfo.myInfo.firmwareVersion))] - version = version.dropLast() - myInfo.firmwareVersion = String(version) - lastConnnectionVersion = String(version) + // MARK: Check for an All / Broadcast User + let fetchBCUserRequest: NSFetchRequest = NSFetchRequest.init(entityName: "UserEntity") + fetchBCUserRequest.predicate = NSPredicate(format: "num == %lld", Int64(broadcastNodeNum)) + + do { + let fetchedUser = try context?.fetch(fetchBCUserRequest) as! [UserEntity] - myInfo.messageTimeoutMsec = Int32(bitPattern: decodedInfo.myInfo.messageTimeoutMsec) - myInfo.minAppVersion = Int32(bitPattern: decodedInfo.myInfo.minAppVersion) - myInfo.maxChannels = Int32(bitPattern: decodedInfo.myInfo.maxChannels) - self.connectedPeripheral.num = myInfo.myNodeNum - self.connectedPeripheral.firmwareVersion = myInfo.firmwareVersion ?? "Unknown" - self.connectedPeripheral.name = myInfo.bleName ?? "Unknown" - - let fetchBCUserRequest: NSFetchRequest = NSFetchRequest.init(entityName: "UserEntity") - fetchBCUserRequest.predicate = NSPredicate(format: "num == %lld", Int64(decodedInfo.myInfo.myNodeNum)) - - do { - let fetchedUser = try context?.fetch(fetchBCUserRequest) as! [UserEntity] - - if fetchedUser.isEmpty { - // Save the broadcast user if it does not exist - let bcu: UserEntity = UserEntity(context: context!) - bcu.shortName = "ALL" - bcu.longName = "All - Broadcast" - bcu.hwModel = "UNSET" - bcu.num = Int64(broadcastNodeNum) - bcu.userId = "BROADCASTNODE" - print("πŸ’Ύ Saved the All - Broadcast User") - } - - } catch { - - print("πŸ’₯ Error Saving the All - Broadcast User") - } - - } else { - - fetchedMyInfo[0].myNodeNum = Int64(decodedInfo.myInfo.myNodeNum) - fetchedMyInfo[0].hasGps = decodedInfo.myInfo.hasGps_p - fetchedMyInfo[0].bitrate = decodedInfo.myInfo.bitrate - - let lastDotIndex = decodedInfo.myInfo.firmwareVersion.lastIndex(of: ".")//.lastIndex(of: ".", offsetBy: -1) - var version = decodedInfo.myInfo.firmwareVersion[...(lastDotIndex ?? String.Index(utf16Offset:6, in: decodedInfo.myInfo.firmwareVersion))] - version = version.dropLast() - fetchedMyInfo[0].firmwareVersion = String(version) - lastConnnectionVersion = String(version) - fetchedMyInfo[0].messageTimeoutMsec = Int32(bitPattern: decodedInfo.myInfo.messageTimeoutMsec) - fetchedMyInfo[0].minAppVersion = Int32(bitPattern: decodedInfo.myInfo.minAppVersion) - fetchedMyInfo[0].maxChannels = Int32(bitPattern: decodedInfo.myInfo.maxChannels) - - self.connectedPeripheral.num = fetchedMyInfo[0].myNodeNum - self.connectedPeripheral.firmwareVersion = fetchedMyInfo[0].firmwareVersion ?? "Unknown" - self.connectedPeripheral.name = fetchedMyInfo[0].bleName ?? "Unknown" - self.connectedPeripheral.bitrate = fetchedMyInfo[0].bitrate - - } - do { - - try context!.save() - if meshLoggingEnabled { MeshLogger.log("πŸ’Ύ Saved a myInfo for \(peripheral.name ?? String(decodedInfo.myInfo.myNodeNum))") } - - } catch { - - context!.rollback() - - let nsError = error as NSError - print("πŸ’₯ Error Saving Core Data MyInfoEntity: \(nsError)") - } - - } catch { - - print("πŸ’₯ Fetch MyInfo Error") + if fetchedUser.isEmpty { + // Save the broadcast user if it does not exist + let bcu: UserEntity = UserEntity(context: context!) + bcu.shortName = "ALL" + bcu.longName = "All - Broadcast" + bcu.hwModel = "UNSET" + bcu.num = Int64(broadcastNodeNum) + bcu.userId = "BROADCASTNODE" + print("πŸ’Ύ Saved the All - Broadcast User") } - // MARK: Share Location Position Update Timer - // Use context to pass the radio name with the timer - // Use a RunLoop to prevent the timer from running on the main UI thread - if userSettings?.provideLocation ?? false { - - if self.positionTimer != nil { - self.positionTimer!.invalidate() - } - let context = ["name": "@\(peripheral.name ?? "Unknown")"] - self.positionTimer = Timer.scheduledTimer(timeInterval: TimeInterval((userSettings?.provideLocationInterval ?? 900)), target: self, selector: #selector(positionTimerFired), userInfo: context, repeats: true) - RunLoop.current.add(self.positionTimer!, forMode: .common) - } + } catch { + + print("πŸ’₯ Error Saving the All - Broadcast User") } - // MARK: Incoming Node Info Packet - if decodedInfo.nodeInfo.num != 0 { - - let fetchNodeRequest: NSFetchRequest = NSFetchRequest.init(entityName: "NodeInfoEntity") - 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 && decodedInfo.nodeInfo.hasUser { - - let newNode = NodeInfoEntity(context: context!) - newNode.id = Int64(decodedInfo.nodeInfo.num) - newNode.num = Int64(decodedInfo.nodeInfo.num) - - if decodedInfo.nodeInfo.hasDeviceMetrics { - - let telemetry = TelemetryEntity(context: context!) - - telemetry.batteryLevel = Int32(decodedInfo.nodeInfo.deviceMetrics.batteryLevel) - telemetry.voltage = decodedInfo.nodeInfo.deviceMetrics.voltage - telemetry.channelUtilization = decodedInfo.nodeInfo.deviceMetrics.channelUtilization - self.connectedPeripheral.channelUtilization = telemetry.channelUtilization - telemetry.airUtilTx = decodedInfo.nodeInfo.deviceMetrics.airUtilTx - self.connectedPeripheral.airTime = decodedInfo.nodeInfo.deviceMetrics.airUtilTx - - var newTelemetries = [TelemetryEntity]() - newTelemetries.append(telemetry) - newNode.telemetries? = NSOrderedSet(array: newTelemetries) - } - - newNode.lastHeard = Date(timeIntervalSince1970: TimeInterval(Int64(decodedInfo.nodeInfo.lastHeard))) - newNode.snr = decodedInfo.nodeInfo.snr - - if self.connectedPeripheral != nil && self.connectedPeripheral.num == newNode.num { - - if decodedInfo.nodeInfo.hasUser { - - connectedPeripheral.name = decodedInfo.nodeInfo.user.longName - } - } - - if decodedInfo.nodeInfo.hasUser { - - 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 - } - - let position = PositionEntity(context: context!) - position.latitudeI = decodedInfo.nodeInfo.position.latitudeI - position.longitudeI = decodedInfo.nodeInfo.position.longitudeI - position.altitude = decodedInfo.nodeInfo.position.altitude - position.time = Date(timeIntervalSince1970: TimeInterval(Int64(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 == %lld", 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 if decodedInfo.nodeInfo.hasUser && decodedInfo.nodeInfo.num > 0 { - - fetchedNode[0].id = Int64(decodedInfo.nodeInfo.num) - fetchedNode[0].num = Int64(decodedInfo.nodeInfo.num) - fetchedNode[0].lastHeard = Date(timeIntervalSince1970: TimeInterval(Int64(decodedInfo.nodeInfo.lastHeard))) - fetchedNode[0].snr = decodedInfo.nodeInfo.snr - - if self.connectedPeripheral != nil && self.connectedPeripheral.num == fetchedNode[0].num { - - if decodedInfo.nodeInfo.hasUser { - - self.connectedPeripheral.name = fetchedNode[0].user!.longName ?? "Unknown" - } - } - - if decodedInfo.nodeInfo.hasUser { - - fetchedNode[0].user!.userId = decodedInfo.nodeInfo.user.id - fetchedNode[0].user!.num = Int64(decodedInfo.nodeInfo.num) - fetchedNode[0].user!.longName = decodedInfo.nodeInfo.user.longName - fetchedNode[0].user!.shortName = decodedInfo.nodeInfo.user.shortName - fetchedNode[0].user!.macaddr = decodedInfo.nodeInfo.user.macaddr - fetchedNode[0].user!.hwModel = String(describing: decodedInfo.nodeInfo.user.hwModel).uppercased() - } - - if decodedInfo.nodeInfo.hasDeviceMetrics { - - let newTelemetry = TelemetryEntity(context: context!) - - newTelemetry.batteryLevel = Int32(decodedInfo.nodeInfo.deviceMetrics.batteryLevel) - newTelemetry.voltage = decodedInfo.nodeInfo.deviceMetrics.voltage - newTelemetry.channelUtilization = decodedInfo.nodeInfo.deviceMetrics.channelUtilization - self.connectedPeripheral.channelUtilization = newTelemetry.channelUtilization - newTelemetry.airUtilTx = decodedInfo.nodeInfo.deviceMetrics.airUtilTx - self.connectedPeripheral.airTime = decodedInfo.nodeInfo.deviceMetrics.airUtilTx - - let mutableTelemetries = fetchedNode[0].telemetries!.mutableCopy() as! NSMutableOrderedSet - fetchedNode[0].telemetries = mutableTelemetries.copy() as? NSOrderedSet - } - - 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.time = Date(timeIntervalSince1970: TimeInterval(Int64(decodedInfo.nodeInfo.position.time))) - - let mutablePositions = fetchedNode[0].positions!.mutableCopy() as! NSMutableOrderedSet - - fetchedNode[0].positions = mutablePositions.copy() as? NSOrderedSet - - } - - // Look for a MyInfo - let fetchMyInfoRequest: NSFetchRequest = NSFetchRequest.init(entityName: "MyInfoEntity") - fetchMyInfoRequest.predicate = NSPredicate(format: "myNodeNum == %lld", Int64(decodedInfo.nodeInfo.num)) - - do { - - let fetchedMyInfo = try context?.fetch(fetchMyInfoRequest) as! [MyInfoEntity] - if fetchedMyInfo.count > 0 { - - fetchedNode[0].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 Core Data NodeInfoEntity: \(nsError)") - } - - } catch { - - print("πŸ’₯ Fetch NodeInfoEntity Error") - } - - if decodedInfo.nodeInfo.hasUser { - - if meshLoggingEnabled { MeshLogger.log("πŸ’Ύ BLE FROMRADIO received and nodeInfo saved for \(decodedInfo.nodeInfo.user.longName)") } - - } else { - - if meshLoggingEnabled { MeshLogger.log("πŸ’Ύ BLE FROMRADIO received and nodeInfo saved for \(decodedInfo.nodeInfo.num)") } + // MARK: Share Location Position Update Timer + // Use context to pass the radio name with the timer + // Use a RunLoop to prevent the timer from running on the main UI thread + if userSettings?.provideLocation ?? false { + + if self.positionTimer != nil { + self.positionTimer!.invalidate() } + let context = ["name": "@\(peripheral.name ?? "Unknown")"] + self.positionTimer = Timer.scheduledTimer(timeInterval: TimeInterval((userSettings?.provideLocationInterval ?? 900)), target: self, selector: #selector(positionTimerFired), userInfo: context, repeats: true) + RunLoop.current.add(self.positionTimer!, forMode: .common) } if decodedInfo.configCompleteID != 0 { @@ -720,7 +513,7 @@ class BLEManager: NSObject, ObservableObject, CBCentralManagerDelegate, CBPeriph case FROMNUM_UUID : - print("πŸ—žοΈ FROMNUM Notification, value will be read below") + print("πŸ—žοΈ BLE FROMNUM (Notify) characteristic, value will be read next") default: @@ -731,7 +524,6 @@ class BLEManager: NSObject, ObservableObject, CBCentralManagerDelegate, CBPeriph peripheral.readValue(for: FROMRADIO_characteristic) } - // Send Message public func sendMessage(message: String, toUserNum: Int64, isEmoji: Bool, replyID: Int64) -> Bool { var success = false @@ -854,7 +646,6 @@ class BLEManager: NSObject, ObservableObject, CBCentralManagerDelegate, CBPeriph return success } - // Send Position public func sendPosition(destNum: Int64, wantResponse: Bool) -> Bool { var success = false @@ -884,7 +675,7 @@ class BLEManager: NSObject, ObservableObject, CBCentralManagerDelegate, CBPeriph var meshPacket = MeshPacket() meshPacket.to = UInt32(destNum) - meshPacket.from = 0 //UInt32(connectedPeripheral.num) + meshPacket.from = 0 // Send 0 as from from phone to device to avoid warning about client trying to set node num meshPacket.wantAck = wantResponse var dataMessage = DataMessage() @@ -933,50 +724,14 @@ class BLEManager: NSObject, ObservableObject, CBCentralManagerDelegate, CBPeriph } } - // MARK: Device Settings - public func getSettings() -> Bool { - - var adminPacket = AdminMessage() - - var meshPacket: MeshPacket = MeshPacket() - meshPacket.to = UInt32(connectedPeripheral.num) - meshPacket.from = UInt32(connectedPeripheral.num) - meshPacket.id = UInt32.random(in: UInt32(UInt8.max).. Bool { var adminPacket = AdminMessage() - adminPacket.rebootSeconds = 30 + adminPacket.shutdownSeconds = 10 var meshPacket: MeshPacket = MeshPacket() meshPacket.to = UInt32(connectedPeripheral.num) - meshPacket.from = UInt32(connectedPeripheral.num) + meshPacket.from = 0 //UInt32(connectedPeripheral.num) meshPacket.id = UInt32.random(in: UInt32(UInt8.max).. Bool { + + var adminPacket = AdminMessage() + adminPacket.rebootSeconds = 10 + + var meshPacket: MeshPacket = MeshPacket() + meshPacket.to = UInt32(connectedPeripheral.num) + meshPacket.from = 0 //UInt32(connectedPeripheral.num) + meshPacket.id = UInt32.random(in: UInt32(UInt8.max).. MyInfoEntity? { + + let fetchMyInfoRequest: NSFetchRequest = NSFetchRequest.init(entityName: "MyInfoEntity") + fetchMyInfoRequest.predicate = NSPredicate(format: "myNodeNum == %lld", Int64(myInfo.myNodeNum)) + do { + let fetchedMyInfo = try context.fetch(fetchMyInfoRequest) as! [MyInfoEntity] + // Not Found Insert + if fetchedMyInfo.isEmpty { + + let myInfoEntity = MyInfoEntity(context: context) + myInfoEntity.myNodeNum = Int64(myInfo.myNodeNum) + myInfoEntity.hasGps = myInfo.hasGps_p + myInfoEntity.hasWifi = myInfo.hasWifi_p + myInfoEntity.bitrate = myInfo.bitrate -func nodeInfoPacket (packet: MeshPacket, meshLogging: Bool, context: NSManagedObjectContext) { + // Swift does strings weird, this does work to get the version without the github hash + let lastDotIndex = myInfo.firmwareVersion.lastIndex(of: ".") + var version = myInfo.firmwareVersion[...(lastDotIndex ?? String.Index(utf16Offset: 6, in: myInfo.firmwareVersion))] + version = version.dropLast() + myInfoEntity.firmwareVersion = String(version) + myInfoEntity.messageTimeoutMsec = Int32(bitPattern: myInfo.messageTimeoutMsec) + myInfoEntity.minAppVersion = Int32(bitPattern: myInfo.minAppVersion) + myInfoEntity.maxChannels = Int32(bitPattern: myInfo.maxChannels) + + do { + + try context.save() + if meshLogging { MeshLogger.log("πŸ’Ύ Saved a new myInfo for node number: \(String(myInfo.myNodeNum))") } + return myInfoEntity + + } catch { + + context.rollback() + + let nsError = error as NSError + print("πŸ’₯ Error Inserting New Core Data MyInfoEntity: \(nsError)") + } + + } else { + + fetchedMyInfo[0].myNodeNum = Int64(myInfo.myNodeNum) + fetchedMyInfo[0].hasGps = myInfo.hasGps_p + fetchedMyInfo[0].bitrate = myInfo.bitrate + + let lastDotIndex = myInfo.firmwareVersion.lastIndex(of: ".")//.lastIndex(of: ".", offsetBy: -1) + var version = myInfo.firmwareVersion[...(lastDotIndex ?? String.Index(utf16Offset:6, in: myInfo.firmwareVersion))] + version = version.dropLast() + fetchedMyInfo[0].firmwareVersion = String(version) + fetchedMyInfo[0].messageTimeoutMsec = Int32(bitPattern: myInfo.messageTimeoutMsec) + fetchedMyInfo[0].minAppVersion = Int32(bitPattern: myInfo.minAppVersion) + fetchedMyInfo[0].maxChannels = Int32(bitPattern: myInfo.maxChannels) + + do { + + try context.save() + if meshLogging { MeshLogger.log("πŸ’Ύ Updated myInfo for node number: \(String(myInfo.myNodeNum))") } + return fetchedMyInfo[0] + + } catch { + + context.rollback() + + let nsError = error as NSError + print("πŸ’₯ Error Updating Core Data MyInfoEntity: \(nsError)") + } + } + + } catch { + + print("πŸ’₯ Fetch MyInfo Error") + } + return nil +} + +func nodeInfoPacket (nodeInfo: NodeInfo, meshLogging: Bool, context: NSManagedObjectContext) -> NodeInfoEntity? { + + let fetchNodeInfoRequest: NSFetchRequest = NSFetchRequest.init(entityName: "NodeInfoEntity") + fetchNodeInfoRequest.predicate = NSPredicate(format: "num == %lld", Int64(nodeInfo.num)) + + do { + + let fetchedNode = try context.fetch(fetchNodeInfoRequest) as! [NodeInfoEntity] + // Not Found Insert + if fetchedNode.isEmpty && nodeInfo.hasUser { + + let newNode = NodeInfoEntity(context: context) + newNode.id = Int64(nodeInfo.num) + newNode.num = Int64(nodeInfo.num) + + if nodeInfo.hasDeviceMetrics { + + let telemetry = TelemetryEntity(context: context) + + telemetry.batteryLevel = Int32(nodeInfo.deviceMetrics.batteryLevel) + telemetry.voltage = nodeInfo.deviceMetrics.voltage + telemetry.channelUtilization = nodeInfo.deviceMetrics.channelUtilization + telemetry.airUtilTx = nodeInfo.deviceMetrics.airUtilTx + + var newTelemetries = [TelemetryEntity]() + newTelemetries.append(telemetry) + newNode.telemetries? = NSOrderedSet(array: newTelemetries) + } + + newNode.lastHeard = Date(timeIntervalSince1970: TimeInterval(Int64(nodeInfo.lastHeard))) + newNode.snr = nodeInfo.snr + + if nodeInfo.hasUser { + + let newUser = UserEntity(context: context) + newUser.userId = nodeInfo.user.id + newUser.num = Int64(nodeInfo.num) + newUser.longName = nodeInfo.user.longName + newUser.shortName = nodeInfo.user.shortName + newUser.macaddr = nodeInfo.user.macaddr + newUser.hwModel = String(describing: nodeInfo.user.hwModel).uppercased() + newNode.user = newUser + } + + let position = PositionEntity(context: context) + position.latitudeI = nodeInfo.position.latitudeI + position.longitudeI = nodeInfo.position.longitudeI + position.altitude = nodeInfo.position.altitude + position.time = Date(timeIntervalSince1970: TimeInterval(Int64(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 == %lld", Int64(nodeInfo.num)) + + do { + + let fetchedMyInfo = try context.fetch(fetchMyInfoRequest) as! [MyInfoEntity] + if fetchedMyInfo.count > 0 { + newNode.myInfo = fetchedMyInfo[0] + } + + do { + + try context.save() + + if nodeInfo.hasUser { + + if meshLogging { MeshLogger.log("πŸ’Ύ BLE FROMRADIO received and nodeInfo inserted for \(nodeInfo.user.longName)") } + + } else { + + if meshLogging { MeshLogger.log("πŸ’Ύ BLE FROMRADIO received and nodeInfo inserted for \(nodeInfo.num)") } + } + return newNode + + } catch { + + context.rollback() + + let nsError = error as NSError + print("πŸ’₯ Error Saving Core Data NodeInfoEntity: \(nsError)") + } + + } catch { + print("πŸ’₯ Fetch MyInfo Error") + } + + } else if nodeInfo.hasUser && nodeInfo.num > 0 { + + fetchedNode[0].id = Int64(nodeInfo.num) + fetchedNode[0].num = Int64(nodeInfo.num) + fetchedNode[0].lastHeard = Date(timeIntervalSince1970: TimeInterval(Int64(nodeInfo.lastHeard))) + fetchedNode[0].snr = nodeInfo.snr + + + if nodeInfo.hasUser { + + fetchedNode[0].user!.userId = nodeInfo.user.id + fetchedNode[0].user!.num = Int64(nodeInfo.num) + fetchedNode[0].user!.longName = nodeInfo.user.longName + fetchedNode[0].user!.shortName = nodeInfo.user.shortName + fetchedNode[0].user!.macaddr = nodeInfo.user.macaddr + fetchedNode[0].user!.hwModel = String(describing: nodeInfo.user.hwModel).uppercased() + } + + if nodeInfo.hasDeviceMetrics { + + let newTelemetry = TelemetryEntity(context: context) + + newTelemetry.batteryLevel = Int32(nodeInfo.deviceMetrics.batteryLevel) + newTelemetry.voltage = nodeInfo.deviceMetrics.voltage + newTelemetry.channelUtilization = nodeInfo.deviceMetrics.channelUtilization + newTelemetry.airUtilTx = nodeInfo.deviceMetrics.airUtilTx + + let mutableTelemetries = fetchedNode[0].telemetries!.mutableCopy() as! NSMutableOrderedSet + fetchedNode[0].telemetries = mutableTelemetries.copy() as? NSOrderedSet + } + + if nodeInfo.hasPosition { + + let position = PositionEntity(context: context) + position.latitudeI = nodeInfo.position.latitudeI + position.longitudeI = nodeInfo.position.longitudeI + position.altitude = nodeInfo.position.altitude + position.time = Date(timeIntervalSince1970: TimeInterval(Int64(nodeInfo.position.time))) + + let mutablePositions = fetchedNode[0].positions!.mutableCopy() as! NSMutableOrderedSet + + fetchedNode[0].positions = mutablePositions.copy() as? NSOrderedSet + + } + + // Look for a MyInfo + let fetchMyInfoRequest: NSFetchRequest = NSFetchRequest.init(entityName: "MyInfoEntity") + fetchMyInfoRequest.predicate = NSPredicate(format: "myNodeNum == %lld", Int64(nodeInfo.num)) + + do { + + let fetchedMyInfo = try context.fetch(fetchMyInfoRequest) as! [MyInfoEntity] + if fetchedMyInfo.count > 0 { + + fetchedNode[0].myInfo = fetchedMyInfo[0] + } + + do { + + try context.save() + + if nodeInfo.hasUser { + + if meshLogging { MeshLogger.log("πŸ’Ύ BLE FROMRADIO received and nodeInfo inserted for \(nodeInfo.user.longName)") } + + } else { + + if meshLogging { MeshLogger.log("πŸ’Ύ BLE FROMRADIO received and nodeInfo inserted for \(nodeInfo.num)") } + } + + return fetchedNode[0] + + } catch { + + context.rollback() + + let nsError = error as NSError + print("πŸ’₯ Error Saving Core Data NodeInfoEntity: \(nsError)") + } + + } catch { + print("πŸ’₯ Fetch MyInfo Error") + } + } + + } catch { + + print("πŸ’₯ Fetch NodeInfoEntity Error") + } + + return nil +} + +func nodeInfoAppPacket (packet: MeshPacket, meshLogging: Bool, context: NSManagedObjectContext) { let fetchNodeInfoAppRequest: NSFetchRequest = NSFetchRequest.init(entityName: "NodeInfoEntity") fetchNodeInfoAppRequest.predicate = NSPredicate(format: "num == %lld", Int64(packet.from)) @@ -24,7 +281,24 @@ func nodeInfoPacket (packet: MeshPacket, meshLogging: Bool, context: NSManagedOb fetchedNode[0].num = Int64(packet.from) fetchedNode[0].lastHeard = Date(timeIntervalSince1970: TimeInterval(Int64(packet.rxTime))) fetchedNode[0].snr = packet.rxSnr - + + if let nodeInfoMessage = try? NodeInfo(serializedData: packet.decoded.payload) { + + if nodeInfoMessage.hasDeviceMetrics { + + let telemetry = TelemetryEntity(context: context) + + telemetry.batteryLevel = Int32(nodeInfoMessage.deviceMetrics.batteryLevel) + telemetry.voltage = nodeInfoMessage.deviceMetrics.voltage + telemetry.channelUtilization = nodeInfoMessage.deviceMetrics.channelUtilization + telemetry.airUtilTx = nodeInfoMessage.deviceMetrics.airUtilTx + + var newTelemetries = [TelemetryEntity]() + newTelemetries.append(telemetry) + fetchedNode[0].telemetries? = NSOrderedSet(array: newTelemetries) + } + } + } else { return } @@ -107,8 +381,6 @@ func positionPacket (packet: MeshPacket, meshLogging: Bool, context: NSManagedOb func routingPacket (packet: MeshPacket, meshLogging: Bool, context: NSManagedObjectContext) { if let routingMessage = try? Routing(serializedData: packet.decoded.payload) { - print(packet.decoded.requestID) - print(routingMessage) let error = routingMessage.errorReason @@ -138,16 +410,13 @@ func routingPacket (packet: MeshPacket, meshLogging: Bool, context: NSManagedObj case Routing.Error.notAuthorized: errorExplanation = "The application layer service on the remote node received your request, but considered your request not authorized (i.e you did not send the request on the required bound channel)" fallthrough - default: - print(error) + default: () } if meshLogging { MeshLogger.log("πŸ•ΈοΈ ROUTING PACKET received for RequestID: \(packet.decoded.requestID) Error: \(errorExplanation)") } if routingMessage.errorReason == Routing.Error.none { - print("Priority ACK no Error") - let fetchMessageRequest: NSFetchRequest = NSFetchRequest.init(entityName: "MessageEntity") fetchMessageRequest.predicate = NSPredicate(format: "messageId == %lld", Int64(packet.decoded.requestID)) @@ -161,6 +430,8 @@ func routingPacket (packet: MeshPacket, meshLogging: Bool, context: NSManagedObj fetchedMessage!.ackSNR = packet.rxSnr fetchedMessage!.ackTimestamp = Int32(packet.rxTime) fetchedMessage!.objectWillChange.send() + fetchedMessage!.fromUser?.objectWillChange.send() + fetchedMessage!.toUser?.objectWillChange.send() } try context.save() @@ -186,7 +457,48 @@ func telemetryPacket(packet: MeshPacket, meshLogging: Bool, context: NSManagedOb let telemetry = TelemetryEntity(context: context) - if meshLogging { MeshLogger.log("ℹ️ MESH PACKET received for Telemetry App UNHANDLED \(telemetryMessage)") } + let fetchNodeTelemetryRequest: NSFetchRequest = NSFetchRequest.init(entityName: "NodeInfoEntity") + fetchNodeTelemetryRequest.predicate = NSPredicate(format: "num == %lld", Int64(packet.from)) + + do { + + let fetchedNode = try context.fetch(fetchNodeTelemetryRequest) as! [NodeInfoEntity] + + if fetchedNode.count == 1 { + + // Device Metrics + telemetry.airUtilTx = telemetryMessage.deviceMetrics.airUtilTx + telemetry.channelUtilization = telemetryMessage.deviceMetrics.channelUtilization + telemetry.batteryLevel = Int32(telemetryMessage.deviceMetrics.batteryLevel) + telemetry.voltage = telemetryMessage.deviceMetrics.voltage + + // Environment Metrics + telemetry.barometricPressure = telemetryMessage.environmentMetrics.barometricPressure + telemetry.current = telemetryMessage.environmentMetrics.current + telemetry.gasResistance = telemetryMessage.environmentMetrics.gasResistance + telemetry.relativeHumidity = telemetryMessage.environmentMetrics.relativeHumidity + telemetry.temperature = telemetryMessage.environmentMetrics.temperature + let mutableTelemetries = fetchedNode[0].telemetries!.mutableCopy() as! NSMutableOrderedSet + mutableTelemetries.add(telemetry) + + fetchedNode[0].lastHeard = Date(timeIntervalSince1970: TimeInterval(Int64(telemetryMessage.time))) + fetchedNode[0].telemetries = mutableTelemetries.copy() as? NSOrderedSet + fetchedNode[0].objectWillChange.send() + } + + try context.save() + + if meshLogging { + MeshLogger.log("πŸ’Ύ Telemetry Saved for Node: \(packet.from)") + } + + } catch { + + context.rollback() + + let nsError = error as NSError + print("πŸ’₯ Error Saving Telemetry for Node \(packet.from) Error: \(nsError)") + } } else { @@ -238,6 +550,8 @@ func textMessageAppPacket(packet: MeshPacket, connectedNode: Int64, meshLogging: newMessage.fromUser = fetchedUsers.first(where: { $0.num == packet.from }) newMessage.messagePayload = messageText + newMessage.fromUser?.objectWillChange.send() + newMessage.toUser?.objectWillChange.send() do { diff --git a/MeshtasticClient/Persistence/UserEntityExtension.swift b/MeshtasticClient/Persistence/UserEntityExtension.swift new file mode 100644 index 00000000..23283eef --- /dev/null +++ b/MeshtasticClient/Persistence/UserEntityExtension.swift @@ -0,0 +1,16 @@ +// +// UserEntityExtension.swift +// MeshtasticClient +// +// Created by Garth Vander Houwen on 6/3/22. +// + +import Foundation + +extension UserEntity { + + var messageList: [MessageEntity] { + + self.value(forKey: "allMessages") as! [MessageEntity] + } +} diff --git a/MeshtasticClient/Protobufs/apponly.pb.swift b/MeshtasticClient/Protobufs/apponly.pb.swift index 687030b7..46ab82c8 100644 --- a/MeshtasticClient/Protobufs/apponly.pb.swift +++ b/MeshtasticClient/Protobufs/apponly.pb.swift @@ -32,12 +32,25 @@ struct ChannelSet { // methods supported on all messages. /// - /// TODO: REPLACE + /// Channel list with settings var settings: [ChannelSettings] = [] + /// + /// LoRa config + var loraConfig: Config.LoRaConfig { + get {return _loraConfig ?? Config.LoRaConfig()} + set {_loraConfig = newValue} + } + /// Returns true if `loraConfig` has been explicitly set. + var hasLoraConfig: Bool {return self._loraConfig != nil} + /// Clears the value of `loraConfig`. Subsequent reads from it will return its default value. + mutating func clearLoraConfig() {self._loraConfig = nil} + var unknownFields = SwiftProtobuf.UnknownStorage() init() {} + + fileprivate var _loraConfig: Config.LoRaConfig? = nil } #if swift(>=5.5) && canImport(_Concurrency) @@ -50,6 +63,7 @@ extension ChannelSet: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementatio static let protoMessageName: String = "ChannelSet" static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ 1: .same(proto: "settings"), + 2: .standard(proto: "lora_config"), ] mutating func decodeMessage(decoder: inout D) throws { @@ -59,20 +73,29 @@ extension ChannelSet: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementatio // enabled. https://github.com/apple/swift-protobuf/issues/1034 switch fieldNumber { case 1: try { try decoder.decodeRepeatedMessageField(value: &self.settings) }() + case 2: try { try decoder.decodeSingularMessageField(value: &self._loraConfig) }() default: break } } } func traverse(visitor: inout V) throws { + // The use of inline closures is to circumvent an issue where the compiler + // allocates stack space for every if/case branch local when no optimizations + // are enabled. https://github.com/apple/swift-protobuf/issues/1034 and + // https://github.com/apple/swift-protobuf/issues/1182 if !self.settings.isEmpty { try visitor.visitRepeatedMessageField(value: self.settings, fieldNumber: 1) } + try { if let v = self._loraConfig { + try visitor.visitSingularMessageField(value: v, fieldNumber: 2) + } }() try unknownFields.traverse(visitor: &visitor) } static func ==(lhs: ChannelSet, rhs: ChannelSet) -> Bool { if lhs.settings != rhs.settings {return false} + if lhs._loraConfig != rhs._loraConfig {return false} if lhs.unknownFields != rhs.unknownFields {return false} return true } diff --git a/MeshtasticClient/Protobufs/config.pb.swift b/MeshtasticClient/Protobufs/config.pb.swift index e411e8ea..4a1bff42 100644 --- a/MeshtasticClient/Protobufs/config.pb.swift +++ b/MeshtasticClient/Protobufs/config.pb.swift @@ -283,20 +283,6 @@ struct Config { /// window. var gpsAttemptTime: UInt32 = 0 - /// - /// Shall we accept 2D GPS fixes? By default, only 3D fixes are accepted - /// (during a 2D fix, altitude values are unreliable and will be excluded) - var gpsAccept2D: Bool = false - - /// - /// GPS maximum DOP accepted (dilution of precision) - /// Set a rejection threshold for GPS readings based on their precision, - /// relative to the GPS rated accuracy (which is typically ~3m) - /// Solutions above this value will be treated as retryable errors! - /// Useful range is between 1 - 64 (3m - <~200m) - /// By default (if zero), accept all GPS readings - var gpsMaxDop: UInt32 = 0 - /// /// Bit field of boolean configuration options for POSITION messages /// (bitwise OR of PositionFlags) @@ -1294,8 +1280,6 @@ extension Config.PositionConfig: SwiftProtobuf.Message, SwiftProtobuf._MessageIm 5: .standard(proto: "gps_disabled"), 6: .standard(proto: "gps_update_interval"), 7: .standard(proto: "gps_attempt_time"), - 8: .standard(proto: "gps_accept_2d"), - 9: .standard(proto: "gps_max_dop"), 10: .standard(proto: "position_flags"), ] @@ -1311,8 +1295,6 @@ extension Config.PositionConfig: SwiftProtobuf.Message, SwiftProtobuf._MessageIm case 5: try { try decoder.decodeSingularBoolField(value: &self.gpsDisabled) }() case 6: try { try decoder.decodeSingularUInt32Field(value: &self.gpsUpdateInterval) }() case 7: try { try decoder.decodeSingularUInt32Field(value: &self.gpsAttemptTime) }() - case 8: try { try decoder.decodeSingularBoolField(value: &self.gpsAccept2D) }() - case 9: try { try decoder.decodeSingularUInt32Field(value: &self.gpsMaxDop) }() case 10: try { try decoder.decodeSingularUInt32Field(value: &self.positionFlags) }() default: break } @@ -1338,12 +1320,6 @@ extension Config.PositionConfig: SwiftProtobuf.Message, SwiftProtobuf._MessageIm if self.gpsAttemptTime != 0 { try visitor.visitSingularUInt32Field(value: self.gpsAttemptTime, fieldNumber: 7) } - if self.gpsAccept2D != false { - try visitor.visitSingularBoolField(value: self.gpsAccept2D, fieldNumber: 8) - } - if self.gpsMaxDop != 0 { - try visitor.visitSingularUInt32Field(value: self.gpsMaxDop, fieldNumber: 9) - } if self.positionFlags != 0 { try visitor.visitSingularUInt32Field(value: self.positionFlags, fieldNumber: 10) } @@ -1357,8 +1333,6 @@ extension Config.PositionConfig: SwiftProtobuf.Message, SwiftProtobuf._MessageIm if lhs.gpsDisabled != rhs.gpsDisabled {return false} if lhs.gpsUpdateInterval != rhs.gpsUpdateInterval {return false} if lhs.gpsAttemptTime != rhs.gpsAttemptTime {return false} - if lhs.gpsAccept2D != rhs.gpsAccept2D {return false} - if lhs.gpsMaxDop != rhs.gpsMaxDop {return false} if lhs.positionFlags != rhs.positionFlags {return false} if lhs.unknownFields != rhs.unknownFields {return false} return true diff --git a/MeshtasticClient/Protobufs/deviceonly.pb.swift b/MeshtasticClient/Protobufs/deviceonly.pb.swift index 95139c11..52a31d33 100644 --- a/MeshtasticClient/Protobufs/deviceonly.pb.swift +++ b/MeshtasticClient/Protobufs/deviceonly.pb.swift @@ -215,180 +215,11 @@ struct OEMStore { init() {} } -struct LocalConfig { - // SwiftProtobuf.Message conformance is added in an extension below. See the - // `Message` and `Message+*Additions` files in the SwiftProtobuf library for - // methods supported on all messages. - - /// - /// TODO: REPLACE - var device: Config.DeviceConfig { - get {return _storage._device ?? Config.DeviceConfig()} - set {_uniqueStorage()._device = newValue} - } - /// Returns true if `device` has been explicitly set. - var hasDevice: Bool {return _storage._device != nil} - /// Clears the value of `device`. Subsequent reads from it will return its default value. - mutating func clearDevice() {_uniqueStorage()._device = nil} - - /// - /// TODO: REPLACE - var position: Config.PositionConfig { - get {return _storage._position ?? Config.PositionConfig()} - set {_uniqueStorage()._position = newValue} - } - /// Returns true if `position` has been explicitly set. - var hasPosition: Bool {return _storage._position != nil} - /// Clears the value of `position`. Subsequent reads from it will return its default value. - mutating func clearPosition() {_uniqueStorage()._position = nil} - - /// - /// TODO: REPLACE - var power: Config.PowerConfig { - get {return _storage._power ?? Config.PowerConfig()} - set {_uniqueStorage()._power = newValue} - } - /// Returns true if `power` has been explicitly set. - var hasPower: Bool {return _storage._power != nil} - /// Clears the value of `power`. Subsequent reads from it will return its default value. - mutating func clearPower() {_uniqueStorage()._power = nil} - - /// - /// TODO: REPLACE - var wifi: Config.WiFiConfig { - get {return _storage._wifi ?? Config.WiFiConfig()} - set {_uniqueStorage()._wifi = newValue} - } - /// Returns true if `wifi` has been explicitly set. - var hasWifi: Bool {return _storage._wifi != nil} - /// Clears the value of `wifi`. Subsequent reads from it will return its default value. - mutating func clearWifi() {_uniqueStorage()._wifi = nil} - - /// - /// TODO: REPLACE - var display: Config.DisplayConfig { - get {return _storage._display ?? Config.DisplayConfig()} - set {_uniqueStorage()._display = newValue} - } - /// Returns true if `display` has been explicitly set. - var hasDisplay: Bool {return _storage._display != nil} - /// Clears the value of `display`. Subsequent reads from it will return its default value. - mutating func clearDisplay() {_uniqueStorage()._display = nil} - - /// - /// TODO: REPLACE - var lora: Config.LoRaConfig { - get {return _storage._lora ?? Config.LoRaConfig()} - set {_uniqueStorage()._lora = newValue} - } - /// Returns true if `lora` has been explicitly set. - var hasLora: Bool {return _storage._lora != nil} - /// Clears the value of `lora`. Subsequent reads from it will return its default value. - mutating func clearLora() {_uniqueStorage()._lora = nil} - - var unknownFields = SwiftProtobuf.UnknownStorage() - - init() {} - - fileprivate var _storage = _StorageClass.defaultInstance -} - -struct LocalModuleConfig { - // SwiftProtobuf.Message conformance is added in an extension below. See the - // `Message` and `Message+*Additions` files in the SwiftProtobuf library for - // methods supported on all messages. - - /// - /// TODO: REPLACE - var mqtt: ModuleConfig.MQTTConfig { - get {return _storage._mqtt ?? ModuleConfig.MQTTConfig()} - set {_uniqueStorage()._mqtt = newValue} - } - /// Returns true if `mqtt` has been explicitly set. - var hasMqtt: Bool {return _storage._mqtt != nil} - /// Clears the value of `mqtt`. Subsequent reads from it will return its default value. - mutating func clearMqtt() {_uniqueStorage()._mqtt = nil} - - /// - /// TODO: REPLACE - var serial: ModuleConfig.SerialConfig { - get {return _storage._serial ?? ModuleConfig.SerialConfig()} - set {_uniqueStorage()._serial = newValue} - } - /// Returns true if `serial` has been explicitly set. - var hasSerial: Bool {return _storage._serial != nil} - /// Clears the value of `serial`. Subsequent reads from it will return its default value. - mutating func clearSerial() {_uniqueStorage()._serial = nil} - - /// - /// TODO: REPLACE - var externalNotification: ModuleConfig.ExternalNotificationConfig { - get {return _storage._externalNotification ?? ModuleConfig.ExternalNotificationConfig()} - set {_uniqueStorage()._externalNotification = newValue} - } - /// Returns true if `externalNotification` has been explicitly set. - var hasExternalNotification: Bool {return _storage._externalNotification != nil} - /// Clears the value of `externalNotification`. Subsequent reads from it will return its default value. - mutating func clearExternalNotification() {_uniqueStorage()._externalNotification = nil} - - /// - /// TODO: REPLACE - var storeForward: ModuleConfig.StoreForwardConfig { - get {return _storage._storeForward ?? ModuleConfig.StoreForwardConfig()} - set {_uniqueStorage()._storeForward = newValue} - } - /// Returns true if `storeForward` has been explicitly set. - var hasStoreForward: Bool {return _storage._storeForward != nil} - /// Clears the value of `storeForward`. Subsequent reads from it will return its default value. - mutating func clearStoreForward() {_uniqueStorage()._storeForward = nil} - - /// - /// TODO: REPLACE - var rangeTest: ModuleConfig.RangeTestConfig { - get {return _storage._rangeTest ?? ModuleConfig.RangeTestConfig()} - set {_uniqueStorage()._rangeTest = newValue} - } - /// Returns true if `rangeTest` has been explicitly set. - var hasRangeTest: Bool {return _storage._rangeTest != nil} - /// Clears the value of `rangeTest`. Subsequent reads from it will return its default value. - mutating func clearRangeTest() {_uniqueStorage()._rangeTest = nil} - - /// - /// TODO: REPLACE - var telemetry: ModuleConfig.TelemetryConfig { - get {return _storage._telemetry ?? ModuleConfig.TelemetryConfig()} - set {_uniqueStorage()._telemetry = newValue} - } - /// Returns true if `telemetry` has been explicitly set. - var hasTelemetry: Bool {return _storage._telemetry != nil} - /// Clears the value of `telemetry`. Subsequent reads from it will return its default value. - mutating func clearTelemetry() {_uniqueStorage()._telemetry = nil} - - /// - /// TODO: REPLACE - var cannedMessage: ModuleConfig.CannedMessageConfig { - get {return _storage._cannedMessage ?? ModuleConfig.CannedMessageConfig()} - set {_uniqueStorage()._cannedMessage = newValue} - } - /// Returns true if `cannedMessage` has been explicitly set. - var hasCannedMessage: Bool {return _storage._cannedMessage != nil} - /// Clears the value of `cannedMessage`. Subsequent reads from it will return its default value. - mutating func clearCannedMessage() {_uniqueStorage()._cannedMessage = nil} - - var unknownFields = SwiftProtobuf.UnknownStorage() - - init() {} - - fileprivate var _storage = _StorageClass.defaultInstance -} - #if swift(>=5.5) && canImport(_Concurrency) extension ScreenFonts: @unchecked Sendable {} extension DeviceState: @unchecked Sendable {} extension ChannelFile: @unchecked Sendable {} extension OEMStore: @unchecked Sendable {} -extension LocalConfig: @unchecked Sendable {} -extension LocalModuleConfig: @unchecked Sendable {} #endif // swift(>=5.5) && canImport(_Concurrency) // MARK: - Code below here is support for the SwiftProtobuf runtime. @@ -612,227 +443,3 @@ extension OEMStore: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationB return true } } - -extension LocalConfig: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { - static let protoMessageName: String = "LocalConfig" - static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ - 1: .same(proto: "device"), - 2: .same(proto: "position"), - 3: .same(proto: "power"), - 4: .same(proto: "wifi"), - 5: .same(proto: "display"), - 6: .same(proto: "lora"), - ] - - fileprivate class _StorageClass { - var _device: Config.DeviceConfig? = nil - var _position: Config.PositionConfig? = nil - var _power: Config.PowerConfig? = nil - var _wifi: Config.WiFiConfig? = nil - var _display: Config.DisplayConfig? = nil - var _lora: Config.LoRaConfig? = nil - - static let defaultInstance = _StorageClass() - - private init() {} - - init(copying source: _StorageClass) { - _device = source._device - _position = source._position - _power = source._power - _wifi = source._wifi - _display = source._display - _lora = source._lora - } - } - - fileprivate mutating func _uniqueStorage() -> _StorageClass { - if !isKnownUniquelyReferenced(&_storage) { - _storage = _StorageClass(copying: _storage) - } - return _storage - } - - mutating func decodeMessage(decoder: inout D) throws { - _ = _uniqueStorage() - try withExtendedLifetime(_storage) { (_storage: _StorageClass) in - while let fieldNumber = try decoder.nextFieldNumber() { - // The use of inline closures is to circumvent an issue where the compiler - // allocates stack space for every case branch when no optimizations are - // enabled. https://github.com/apple/swift-protobuf/issues/1034 - switch fieldNumber { - case 1: try { try decoder.decodeSingularMessageField(value: &_storage._device) }() - case 2: try { try decoder.decodeSingularMessageField(value: &_storage._position) }() - case 3: try { try decoder.decodeSingularMessageField(value: &_storage._power) }() - case 4: try { try decoder.decodeSingularMessageField(value: &_storage._wifi) }() - case 5: try { try decoder.decodeSingularMessageField(value: &_storage._display) }() - case 6: try { try decoder.decodeSingularMessageField(value: &_storage._lora) }() - default: break - } - } - } - } - - func traverse(visitor: inout V) throws { - try withExtendedLifetime(_storage) { (_storage: _StorageClass) in - // The use of inline closures is to circumvent an issue where the compiler - // allocates stack space for every if/case branch local when no optimizations - // are enabled. https://github.com/apple/swift-protobuf/issues/1034 and - // https://github.com/apple/swift-protobuf/issues/1182 - try { if let v = _storage._device { - try visitor.visitSingularMessageField(value: v, fieldNumber: 1) - } }() - try { if let v = _storage._position { - try visitor.visitSingularMessageField(value: v, fieldNumber: 2) - } }() - try { if let v = _storage._power { - try visitor.visitSingularMessageField(value: v, fieldNumber: 3) - } }() - try { if let v = _storage._wifi { - try visitor.visitSingularMessageField(value: v, fieldNumber: 4) - } }() - try { if let v = _storage._display { - try visitor.visitSingularMessageField(value: v, fieldNumber: 5) - } }() - try { if let v = _storage._lora { - try visitor.visitSingularMessageField(value: v, fieldNumber: 6) - } }() - } - try unknownFields.traverse(visitor: &visitor) - } - - static func ==(lhs: LocalConfig, rhs: LocalConfig) -> Bool { - if lhs._storage !== rhs._storage { - let storagesAreEqual: Bool = withExtendedLifetime((lhs._storage, rhs._storage)) { (_args: (_StorageClass, _StorageClass)) in - let _storage = _args.0 - let rhs_storage = _args.1 - if _storage._device != rhs_storage._device {return false} - if _storage._position != rhs_storage._position {return false} - if _storage._power != rhs_storage._power {return false} - if _storage._wifi != rhs_storage._wifi {return false} - if _storage._display != rhs_storage._display {return false} - if _storage._lora != rhs_storage._lora {return false} - return true - } - if !storagesAreEqual {return false} - } - if lhs.unknownFields != rhs.unknownFields {return false} - return true - } -} - -extension LocalModuleConfig: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { - static let protoMessageName: String = "LocalModuleConfig" - static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ - 1: .same(proto: "mqtt"), - 2: .same(proto: "serial"), - 3: .standard(proto: "external_notification"), - 4: .standard(proto: "store_forward"), - 5: .standard(proto: "range_test"), - 6: .same(proto: "telemetry"), - 7: .standard(proto: "canned_message"), - ] - - fileprivate class _StorageClass { - var _mqtt: ModuleConfig.MQTTConfig? = nil - var _serial: ModuleConfig.SerialConfig? = nil - var _externalNotification: ModuleConfig.ExternalNotificationConfig? = nil - var _storeForward: ModuleConfig.StoreForwardConfig? = nil - var _rangeTest: ModuleConfig.RangeTestConfig? = nil - var _telemetry: ModuleConfig.TelemetryConfig? = nil - var _cannedMessage: ModuleConfig.CannedMessageConfig? = nil - - static let defaultInstance = _StorageClass() - - private init() {} - - init(copying source: _StorageClass) { - _mqtt = source._mqtt - _serial = source._serial - _externalNotification = source._externalNotification - _storeForward = source._storeForward - _rangeTest = source._rangeTest - _telemetry = source._telemetry - _cannedMessage = source._cannedMessage - } - } - - fileprivate mutating func _uniqueStorage() -> _StorageClass { - if !isKnownUniquelyReferenced(&_storage) { - _storage = _StorageClass(copying: _storage) - } - return _storage - } - - mutating func decodeMessage(decoder: inout D) throws { - _ = _uniqueStorage() - try withExtendedLifetime(_storage) { (_storage: _StorageClass) in - while let fieldNumber = try decoder.nextFieldNumber() { - // The use of inline closures is to circumvent an issue where the compiler - // allocates stack space for every case branch when no optimizations are - // enabled. https://github.com/apple/swift-protobuf/issues/1034 - switch fieldNumber { - case 1: try { try decoder.decodeSingularMessageField(value: &_storage._mqtt) }() - case 2: try { try decoder.decodeSingularMessageField(value: &_storage._serial) }() - case 3: try { try decoder.decodeSingularMessageField(value: &_storage._externalNotification) }() - case 4: try { try decoder.decodeSingularMessageField(value: &_storage._storeForward) }() - case 5: try { try decoder.decodeSingularMessageField(value: &_storage._rangeTest) }() - case 6: try { try decoder.decodeSingularMessageField(value: &_storage._telemetry) }() - case 7: try { try decoder.decodeSingularMessageField(value: &_storage._cannedMessage) }() - default: break - } - } - } - } - - func traverse(visitor: inout V) throws { - try withExtendedLifetime(_storage) { (_storage: _StorageClass) in - // The use of inline closures is to circumvent an issue where the compiler - // allocates stack space for every if/case branch local when no optimizations - // are enabled. https://github.com/apple/swift-protobuf/issues/1034 and - // https://github.com/apple/swift-protobuf/issues/1182 - try { if let v = _storage._mqtt { - try visitor.visitSingularMessageField(value: v, fieldNumber: 1) - } }() - try { if let v = _storage._serial { - try visitor.visitSingularMessageField(value: v, fieldNumber: 2) - } }() - try { if let v = _storage._externalNotification { - try visitor.visitSingularMessageField(value: v, fieldNumber: 3) - } }() - try { if let v = _storage._storeForward { - try visitor.visitSingularMessageField(value: v, fieldNumber: 4) - } }() - try { if let v = _storage._rangeTest { - try visitor.visitSingularMessageField(value: v, fieldNumber: 5) - } }() - try { if let v = _storage._telemetry { - try visitor.visitSingularMessageField(value: v, fieldNumber: 6) - } }() - try { if let v = _storage._cannedMessage { - try visitor.visitSingularMessageField(value: v, fieldNumber: 7) - } }() - } - try unknownFields.traverse(visitor: &visitor) - } - - static func ==(lhs: LocalModuleConfig, rhs: LocalModuleConfig) -> Bool { - if lhs._storage !== rhs._storage { - let storagesAreEqual: Bool = withExtendedLifetime((lhs._storage, rhs._storage)) { (_args: (_StorageClass, _StorageClass)) in - let _storage = _args.0 - let rhs_storage = _args.1 - if _storage._mqtt != rhs_storage._mqtt {return false} - if _storage._serial != rhs_storage._serial {return false} - if _storage._externalNotification != rhs_storage._externalNotification {return false} - if _storage._storeForward != rhs_storage._storeForward {return false} - if _storage._rangeTest != rhs_storage._rangeTest {return false} - if _storage._telemetry != rhs_storage._telemetry {return false} - if _storage._cannedMessage != rhs_storage._cannedMessage {return false} - return true - } - if !storagesAreEqual {return false} - } - if lhs.unknownFields != rhs.unknownFields {return false} - return true - } -} diff --git a/MeshtasticClient/Protobufs/portnums.pb.swift b/MeshtasticClient/Protobufs/portnums.pb.swift index 1f63debc..10cef65e 100644 --- a/MeshtasticClient/Protobufs/portnums.pb.swift +++ b/MeshtasticClient/Protobufs/portnums.pb.swift @@ -77,6 +77,10 @@ enum PortNum: SwiftProtobuf.Enum { /// Compressed TEXT_MESSAGE payloads. case textMessageCompressedApp // = 7 + /// + /// Waypoint payloads. + case waypointApp // = 8 + /// /// Provides a 'ping' service that replies to any packet it receives. /// Also serves as a small example module. @@ -143,6 +147,7 @@ enum PortNum: SwiftProtobuf.Enum { case 5: self = .routingApp case 6: self = .adminApp case 7: self = .textMessageCompressedApp + case 8: self = .waypointApp case 32: self = .replyApp case 33: self = .ipTunnelApp case 64: self = .serialApp @@ -167,6 +172,7 @@ enum PortNum: SwiftProtobuf.Enum { case .routingApp: return 5 case .adminApp: return 6 case .textMessageCompressedApp: return 7 + case .waypointApp: return 8 case .replyApp: return 32 case .ipTunnelApp: return 33 case .serialApp: return 64 @@ -196,6 +202,7 @@ extension PortNum: CaseIterable { .routingApp, .adminApp, .textMessageCompressedApp, + .waypointApp, .replyApp, .ipTunnelApp, .serialApp, @@ -227,6 +234,7 @@ extension PortNum: SwiftProtobuf._ProtoNameProviding { 5: .same(proto: "ROUTING_APP"), 6: .same(proto: "ADMIN_APP"), 7: .same(proto: "TEXT_MESSAGE_COMPRESSED_APP"), + 8: .same(proto: "WAYPOINT_APP"), 32: .same(proto: "REPLY_APP"), 33: .same(proto: "IP_TUNNEL_APP"), 64: .same(proto: "SERIAL_APP"), diff --git a/MeshtasticClient/Views/Bluetooth/Connect.swift b/MeshtasticClient/Views/Bluetooth/Connect.swift index 7b4ce84a..2f0f5073 100644 --- a/MeshtasticClient/Views/Bluetooth/Connect.swift +++ b/MeshtasticClient/Views/Bluetooth/Connect.swift @@ -24,7 +24,7 @@ struct Connect: View { var body: some View { let firmwareVersion = bleManager.lastConnnectionVersion - let minimumVersion = "1.3.0" + let minimumVersion = "1.3.15" let supportedVersion = firmwareVersion == "0.0.0" || minimumVersion.compare(firmwareVersion, options: .numeric) == .orderedAscending || minimumVersion.compare(firmwareVersion, options: .numeric) == .orderedSame NavigationView { diff --git a/MeshtasticClient/Views/Helpers/DateTimeText.swift b/MeshtasticClient/Views/Helpers/DateTimeText.swift new file mode 100644 index 00000000..523f6445 --- /dev/null +++ b/MeshtasticClient/Views/Helpers/DateTimeText.swift @@ -0,0 +1,30 @@ +// +// DateTimeText.swift +// MeshtasticClient +// +// Created by Garth Vander Houwen on 5/30/22. +// + +import SwiftUI +// +// LastHeardText.swift +// Meshtastic Apple +// +// Created by Garth Vander Houwen on 5/25/22. +// +struct DateTimeText: View { + var dateTime: Date? + + let sixMonthsAgo = Calendar.current.date(byAdding: .month, value: -6, to: Date()) + + var body: some View { + if (dateTime != nil && dateTime! >= sixMonthsAgo!){ + + Text("\(dateTime!, style: .date) \(dateTime!, style: .time)") + + } else { + + Text("Unknown Age") + } + } +} diff --git a/MeshtasticClient/Views/Helpers/NodeAnnotation.swift b/MeshtasticClient/Views/Helpers/NodeAnnotation.swift new file mode 100644 index 00000000..a8c47579 --- /dev/null +++ b/MeshtasticClient/Views/Helpers/NodeAnnotation.swift @@ -0,0 +1,26 @@ +import Foundation +import SwiftUI + +struct NodeAnnotation: View { + let time: Date + + var body: some View { + + VStack(spacing: 0) { + Text(time, style: .offset) + .font(.callout).foregroundColor(.accentColor) + .padding(5) + .background(Color(.white)) + .cornerRadius(10) + + Image(systemName: "mappin.circle.fill") + .font(.largeTitle) + .foregroundColor(.accentColor) + + Image(systemName: "arrowtriangle.down.fill") + .font(.caption) + .foregroundColor(.accentColor) + .offset(x: 0, y: -5) + } + } +} diff --git a/MeshtasticClient/Views/Messages/UserMessageList.swift b/MeshtasticClient/Views/Messages/UserMessageList.swift index 80ae8b81..98340563 100644 --- a/MeshtasticClient/Views/Messages/UserMessageList.swift +++ b/MeshtasticClient/Views/Messages/UserMessageList.swift @@ -31,7 +31,8 @@ struct UserMessageList: View { @State private var replyMessageId: Int64 = 0 @State private var sendPositionWithMessage: Bool = false - @State var messageCount = 0 + @State private var messageCount = 0 + @State private var refreshId = UUID() var body: some View { @@ -40,20 +41,14 @@ struct UserMessageList: View { let hasTapbackSupport = minimumVersion.compare(firmwareVersion, options: .numeric) == .orderedAscending || minimumVersion.compare(firmwareVersion, options: .numeric) == .orderedSame VStack { - - let allMessages = user.value(forKey: "allMessages") as! [MessageEntity] ScrollViewReader { scrollView in ScrollView { - if allMessages.count > 0 { - - HStack{ - // Padding at the top of the message list - }.padding(.bottom) - - ForEach( allMessages ) { (message: MessageEntity) in + if user.messageList.count > 0 { + + ForEach( user.messageList ) { (message: MessageEntity) in let currentUser: Bool = (bleManager.connectedPeripheral == nil) ? false : ((bleManager.connectedPeripheral != nil && bleManager.connectedPeripheral.num == message.fromUser?.num) ? true : false ) @@ -61,7 +56,7 @@ struct UserMessageList: View { if message.replyID > 0 { - let messageReply = allMessages.first(where: { $0.messageId == message.replyID }) + let messageReply = user.messageList.first(where: { $0.messageId == message.replyID }) HStack { @@ -299,12 +294,13 @@ struct UserMessageList: View { } .padding(.bottom) - .id(allMessages.firstIndex(of: message)) + .id(user.messageList.firstIndex(of: message)) if !currentUser { Spacer(minLength:50) } } + .id(message.messageId) .padding([.leading, .trailing]) .frame(maxWidth: .infinity) .alert(isPresented: $showDeleteMessageAlert) { @@ -313,7 +309,7 @@ struct UserMessageList: View { print("OK button tapped") if deleteMessageId > 0 { - let message = allMessages.first(where: { $0.messageId == deleteMessageId }) + let message = user.messageList.first(where: { $0.messageId == deleteMessageId }) context.delete(message!) do { @@ -338,19 +334,19 @@ struct UserMessageList: View { self.bleManager.context = context self.bleManager.userSettings = userSettings - - if allMessages.count > 1 { - - scrollView.scrollTo(allMessages.firstIndex(of: allMessages.last! ), anchor: .bottom) - } + + messageCount = user.messageList.count + refreshId = UUID() + }) - .onChange(of: allMessages.count, perform: { count in + .onChange(of: messageCount, perform: { value in + //scrollView.scrollTo(user.messageList.firstIndex(of: user.messageList.last! ), anchor: .bottom) + scrollView.scrollTo(user.messageList.last!.messageId) + }) + .onChange(of: user.messageList, perform: { messages in - if count > 1 { - - scrollView.scrollTo(allMessages.firstIndex(of: allMessages.last! ), anchor: .bottom) - - } + refreshId = UUID() + messageCount = messages.count }) } diff --git a/MeshtasticClient/Views/Nodes/NodeDetail.swift b/MeshtasticClient/Views/Nodes/NodeDetail.swift index 5a52d5d3..db5a979e 100644 --- a/MeshtasticClient/Views/Nodes/NodeDetail.swift +++ b/MeshtasticClient/Views/Nodes/NodeDetail.swift @@ -12,6 +12,9 @@ struct NodeDetail: View { @Environment(\.managedObjectContext) var context @EnvironmentObject var bleManager: BLEManager @EnvironmentObject var userSettings: UserSettings + + @State private var isPresentingShutdownConfirm: Bool = false + @State private var isPresentingRebootConfirm: Bool = false var node: NodeInfoEntity @@ -56,7 +59,7 @@ struct NodeDetail: View { content: { - CircleText(text: node.user!.shortName ?? "???", color: .accentColor) + NodeAnnotation(time: location.time!) } ) } @@ -80,8 +83,66 @@ struct NodeDetail: View { } ScrollView { + + HStack { + if self.bleManager.connectedPeripheral != nil && self.bleManager.connectedPeripheral.num == node.num && self.bleManager.connectedPeripheral.num == node.num { + + Button(action: { + + isPresentingShutdownConfirm = true + }) { + + Image(systemName: "power") + .symbolRenderingMode(.hierarchical) + .imageScale(.small) + .foregroundColor(Color.accentColor) + Text("Power Off") + .font(.caption) + + } + .padding() + .background(Color(.systemGray6)) + .clipShape(Capsule()) + .confirmationDialog( + "Are you sure?", + isPresented: $isPresentingShutdownConfirm + ) { + Button("Shutdown Node?", role: .destructive) { + let success = bleManager.sendShutdown(destNum: node.num, wantResponse: false) + } + } + + + Button(action: { + + isPresentingRebootConfirm = true + + }) { + + Image(systemName: "arrow.triangle.2.circlepath") + .symbolRenderingMode(.hierarchical) + .imageScale(.small) + .foregroundColor(Color.accentColor) + Text("Reboot") + .font(.caption) - + } + .padding() + .background(Color(.systemGray6)) + .clipShape(Capsule()) + .confirmationDialog( + "Are you sure?", + isPresented: $isPresentingRebootConfirm + ) { + Button("Reboot Node?", role: .destructive) { + let success = bleManager.sendReboot(destNum: node.num, wantResponse: false) + } + } + } + + } + .padding(5) + Divider() HStack { Image(systemName: "clock.badge.checkmark.fill") @@ -254,7 +315,7 @@ struct NodeDetail: View { .symbolRenderingMode(.hierarchical) Text("Time:") .font(.caption) - Text("\(mappin.time!, style: .date) \(mappin.time!, style: .time)") + DateTimeText(dateTime: mappin.time) .foregroundColor(.gray) .font(.caption) Divider() diff --git a/MeshtasticClient/Views/Settings/ShareChannel.swift b/MeshtasticClient/Views/Settings/ShareChannel.swift index 1756d719..1e252d7a 100644 --- a/MeshtasticClient/Views/Settings/ShareChannel.swift +++ b/MeshtasticClient/Views/Settings/ShareChannel.swift @@ -36,8 +36,9 @@ struct ShareChannel: View { @EnvironmentObject var bleManager: BLEManager @EnvironmentObject var userSettings: UserSettings + let channelSet = ChannelSet() - @State private var text = "meshtastic.org" + @State private var text = "https://www.meshtastic.org/e/#" var qrCodeImage = QrCodeImage() var body: some View {