diff --git a/Localizable.xcstrings b/Localizable.xcstrings index f1806ddf7..e6c292718 100644 --- a/Localizable.xcstrings +++ b/Localizable.xcstrings @@ -140,6 +140,16 @@ }, "%@%%" : { + }, + "%@%% %@%%" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "%1$@%% %2$@%%" + } + } + } }, "%@°F" : { @@ -6325,7 +6335,7 @@ "Direct Message Help" : { }, - "Direct messages are using the new public key infrastructure for encryption. Reguires firmware version 2.5 or greater." : { + "Direct messages are using the new public key infrastructure for encryption. Requires firmware version 2.5 or greater." : { }, "Direct messages are using the shared key for the channel." : { @@ -6712,6 +6722,9 @@ }, "Drag & Drop is the recommended way to update firmware for NRF devices. If your iPhone or iPad is USB-C it will work with your regular USB-C charging cable, for lightning devices you need the Apple Lightning to USB camera adaptor." : { + }, + "Dupe / Bad Packets: %d" : { + }, "echo" : { "localizations" : { @@ -7227,12 +7240,12 @@ }, "Favorites" : { - }, - "Fetch the latest position of a cetain node" : { - }, "Favorites and nodes with recent messages show up at the top of the contact list." : { - + + }, + "Fetch the latest position of a cetain node" : { + }, "Fifteen Minutes" : { @@ -14565,9 +14578,10 @@ }, "Message content exceeds 228 bytes." : { + }, "Message Status Options" : { - + }, "message.details" : { "localizations" : { @@ -16071,6 +16085,12 @@ }, "Override automatic OLED screen detection." : { + }, + "Packets Received: %d" : { + + }, + "Packets Sent: %d" : { + }, "password" : { "localizations" : { @@ -17386,6 +17406,9 @@ }, "Requires that there be an accelerometer on your device." : { + }, + "Reset App Settings" : { + }, "Reset NodeDB" : { @@ -22241,7 +22264,7 @@ } } }, - "Updated Device Metrics Data." : { + "Updated Node Stats Data." : { }, "Updated: %@" : { @@ -22678,7 +22701,7 @@ "Your Firmware is up to date" : { }, - "Your MQTT Server must support TLS." : { + "Your MQTT Server must support TLS. Not available via the public mqtt server." : { }, "Your position has been sent with a request for a response with their position. You will receive a notification when a position is returned." : { diff --git a/Meshtastic.xcodeproj/project.pbxproj b/Meshtastic.xcodeproj/project.pbxproj index 6b02e8f19..dcb2a6004 100644 --- a/Meshtastic.xcodeproj/project.pbxproj +++ b/Meshtastic.xcodeproj/project.pbxproj @@ -368,6 +368,7 @@ DD77093C2AA1AFA3007A8BF0 /* ChannelTips.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChannelTips.swift; sourceTree = ""; }; DD77093E2AA1B146007A8BF0 /* UIColor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIColor.swift; sourceTree = ""; }; DD798B062915928D005217CD /* ChannelMessageList.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChannelMessageList.swift; sourceTree = ""; }; + DD7E235F2C7AA3E50078ACDF /* MeshtasticDataModelV 43.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = "MeshtasticDataModelV 43.xcdatamodel"; sourceTree = ""; }; DD8169F8271F1A6100F4AB02 /* MeshLogger.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MeshLogger.swift; sourceTree = ""; }; DD8169FA271F1F3A00F4AB02 /* MeshLog.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MeshLog.swift; sourceTree = ""; }; DD8169FE272476C700F4AB02 /* LogDocument.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LogDocument.swift; sourceTree = ""; }; @@ -1881,6 +1882,7 @@ DD3CC6BA28E366DF00FA9159 /* Meshtastic.xcdatamodeld */ = { isa = XCVersionGroup; children = ( + DD7E235F2C7AA3E50078ACDF /* MeshtasticDataModelV 43.xcdatamodel */, DD1BD0F12C61D3AD008C0C70 /* MeshtasticDataModelV 42.xcdatamodel */, DD2984A82C5AEF7500B1268D /* MeshtasticDataModelV 41.xcdatamodel */, DD68BAE72C417A74004C01A0 /* MeshtasticDataModelV 40.xcdatamodel */, @@ -1924,7 +1926,7 @@ DD5D0A9A2931AD6B00F7EA61 /* MeshtasticDataModelV2.xcdatamodel */, DD3CC6BB28E366DF00FA9159 /* MeshtasticDataModel.xcdatamodel */, ); - currentVersion = DD1BD0F12C61D3AD008C0C70 /* MeshtasticDataModelV 42.xcdatamodel */; + currentVersion = DD7E235F2C7AA3E50078ACDF /* MeshtasticDataModelV 43.xcdatamodel */; name = Meshtastic.xcdatamodeld; path = Meshtastic/Meshtastic.xcdatamodeld; sourceTree = ""; diff --git a/Meshtastic/Extensions/CoreData/NodeInfoEntityExtension.swift b/Meshtastic/Extensions/CoreData/NodeInfoEntityExtension.swift index 07ee9117c..c1bd5beca 100644 --- a/Meshtastic/Extensions/CoreData/NodeInfoEntityExtension.swift +++ b/Meshtastic/Extensions/CoreData/NodeInfoEntityExtension.swift @@ -52,8 +52,8 @@ extension NodeInfoEntity { } var isOnline: Bool { - let fifteenMinutesAgo = Calendar.current.date(byAdding: .minute, value: -15, to: Date()) - if lastHeard?.compare(fifteenMinutesAgo!) == .orderedDescending { + let twoHoursAgo = Calendar.current.date(byAdding: .minute, value: -120, to: Date()) + if lastHeard?.compare(twoHoursAgo!) == .orderedDescending { return true } return false diff --git a/Meshtastic/Helpers/MeshPackets.swift b/Meshtastic/Helpers/MeshPackets.swift index d3bef97af..12ed1f1bb 100644 --- a/Meshtastic/Helpers/MeshPackets.swift +++ b/Meshtastic/Helpers/MeshPackets.swift @@ -681,14 +681,10 @@ func telemetryPacket(packet: MeshPacket, connectedNode: Int64, context: NSManage if let telemetryMessage = try? Telemetry(serializedData: packet.decoded.payload) { - // Only log telemetry from the mesh not the connected device - if connectedNode != Int64(packet.from) { - let logString = String.localizedStringWithFormat("mesh.log.telemetry.received %@".localized, String(packet.from)) - MeshLogger.log("📈 \(logString)") - } else { - // If it is the connected node - } - if telemetryMessage.variant != Telemetry.OneOf_Variant.deviceMetrics(telemetryMessage.deviceMetrics) && telemetryMessage.variant != Telemetry.OneOf_Variant.environmentMetrics(telemetryMessage.environmentMetrics) { + let logString = String.localizedStringWithFormat("mesh.log.telemetry.received %@".localized, String(packet.from)) + MeshLogger.log("📈 \(logString)") + + if telemetryMessage.variant != Telemetry.OneOf_Variant.deviceMetrics(telemetryMessage.deviceMetrics) && telemetryMessage.variant != Telemetry.OneOf_Variant.environmentMetrics(telemetryMessage.environmentMetrics) && telemetryMessage.variant != Telemetry.OneOf_Variant.localStats(telemetryMessage.localStats) { /// Other unhandled telemetry packets return } @@ -727,6 +723,18 @@ func telemetryPacket(packet: MeshPacket, connectedNode: Int64, context: NSManage telemetry.windLull = telemetryMessage.environmentMetrics.windLull telemetry.windDirection = Int32(truncatingIfNeeded: telemetryMessage.environmentMetrics.windDirection) telemetry.metricsType = 1 + } else if telemetryMessage.variant == Telemetry.OneOf_Variant.localStats(telemetryMessage.localStats) { + // Local Stats for Live activity + telemetry.uptimeSeconds = Int32(telemetryMessage.localStats.uptimeSeconds) + telemetry.channelUtilization = telemetryMessage.localStats.channelUtilization + telemetry.airUtilTx = telemetryMessage.localStats.airUtilTx + telemetry.numPacketsTx = Int32(truncatingIfNeeded: telemetryMessage.localStats.numPacketsTx) + telemetry.numPacketsRx = Int32(truncatingIfNeeded: telemetryMessage.localStats.numPacketsRx) + telemetry.numPacketsRxBad = Int32(truncatingIfNeeded: telemetryMessage.localStats.numPacketsRxBad) + telemetry.numOnlineNodes = Int32(truncatingIfNeeded: telemetryMessage.localStats.numOnlineNodes) + telemetry.numTotalNodes = Int32(truncatingIfNeeded: telemetryMessage.localStats.numTotalNodes) + telemetry.metricsType = 6 + Logger.statistics.info("📈 [Mesh Statistics] Channel Utilization: \(telemetryMessage.localStats.channelUtilization, privacy: .public) Airtime: \(telemetryMessage.localStats.airUtilTx, privacy: .public) Packets Sent: \(telemetryMessage.localStats.numPacketsTx, privacy: .public) Packets Received: \(telemetryMessage.localStats.numPacketsRx, privacy: .public) Bad Packets Received: \(telemetryMessage.localStats.numPacketsRxBad, privacy: .public) Nodes Online: \(telemetryMessage.localStats.numOnlineNodes, privacy: .public) of \(telemetryMessage.localStats.numTotalNodes, privacy: .public) nodes for Node: \(packet.from.toHex(), privacy: .public)") } telemetry.snr = packet.rxSnr telemetry.rssi = packet.rxRssi @@ -743,34 +751,45 @@ func telemetryPacket(packet: MeshPacket, connectedNode: Int64, context: NSManage fetchedNode[0].telemetries = mutableTelemetries.copy() as? NSOrderedSet } try context.save() - // Only log telemetry from the mesh not the connected device - if connectedNode != Int64(packet.from) { - Logger.data.info("💾 [TelemetryEntity] Saved for Node: \(packet.from.toHex())") - } else if telemetry.metricsType == 0 { + + Logger.data.info("💾 [TelemetryEntity] Saved for Node: \(packet.from.toHex())") + if telemetry.metricsType == 0 { // Connected Device Metrics // ------------------------ // Low Battery notification - if UserDefaults.lowBatteryNotifications && telemetry.batteryLevel > 0 && telemetry.batteryLevel < 4 { - let manager = LocalNotificationManager() - manager.notifications = [ - Notification( - id: ("notification.id.\(UUID().uuidString)"), - title: "Critically Low Battery!", - subtitle: "AKA \(telemetry.nodeTelemetry?.user?.shortName ?? "UNK")", - content: "Time to charge your radio, there is \(telemetry.batteryLevel)% battery remaining.", - target: "nodes", - path: "meshtastic:///nodes?nodenum=\(telemetry.nodeTelemetry?.num ?? 0)" - ) - ] - manager.schedule() + if connectedNode != Int64(packet.from) { + if UserDefaults.lowBatteryNotifications && telemetry.batteryLevel > 0 && telemetry.batteryLevel < 4 { + let manager = LocalNotificationManager() + manager.notifications = [ + Notification( + id: ("notification.id.\(UUID().uuidString)"), + title: "Critically Low Battery!", + subtitle: "AKA \(telemetry.nodeTelemetry?.user?.shortName ?? "UNK")", + content: "Time to charge your radio, there is \(telemetry.batteryLevel)% battery remaining.", + target: "nodes", + path: "meshtastic:///nodes?nodenum=\(telemetry.nodeTelemetry?.num ?? 0)" + ) + ] + manager.schedule() + } } + } else if telemetry.metricsType == 6 { // Update our live activity if there is one running, not available on mac iOS >= 16.2 #if !targetEnvironment(macCatalyst) - let oneMinuteLater = Calendar.current.date(byAdding: .minute, value: (Int(1) ), to: Date())! - let date = Date.now...oneMinuteLater - let updatedMeshStatus = MeshActivityAttributes.MeshActivityStatus(timerRange: date, connected: true, channelUtilization: telemetry.channelUtilization, airtime: telemetry.airUtilTx, batteryLevel: UInt32(telemetry.batteryLevel), nodes: 17, nodesOnline: 9) - let alertConfiguration = AlertConfiguration(title: "Mesh activity update", body: "Updated Device Metrics Data.", sound: .default) + let fifteenMinutesLater = Calendar.current.date(byAdding: .minute, value: (Int(15) ), to: Date())! + let date = Date.now...fifteenMinutesLater + let updatedMeshStatus = MeshActivityAttributes.MeshActivityStatus(uptimeSeconds: UInt32(telemetry.uptimeSeconds), + channelUtilization: telemetry.channelUtilization, + airtime: telemetry.airUtilTx, + sentPackets: UInt32(telemetry.numPacketsTx), + receivedPackets: UInt32(telemetry.numPacketsRx), + badReceivedPackets: UInt32(telemetry.numPacketsRxBad), + nodesOnline: UInt32(telemetry.numOnlineNodes), + totalNodes: UInt32(telemetry.numTotalNodes), + timerRange: date) + + let alertConfiguration = AlertConfiguration(title: "Mesh activity update", body: "Updated Node Stats Data.", sound: .default) let updatedContent = ActivityContent(state: updatedMeshStatus, staleDate: nil) let meshActivity = Activity.activities.first(where: { $0.attributes.nodeNum == connectedNode }) diff --git a/Meshtastic/Meshtastic.xcdatamodeld/.xccurrentversion b/Meshtastic/Meshtastic.xcdatamodeld/.xccurrentversion index 3ddf90f8d..04d3cc1a5 100644 --- a/Meshtastic/Meshtastic.xcdatamodeld/.xccurrentversion +++ b/Meshtastic/Meshtastic.xcdatamodeld/.xccurrentversion @@ -3,6 +3,6 @@ _XCCurrentVersionName - MeshtasticDataModelV 42.xcdatamodel + MeshtasticDataModelV 43.xcdatamodel diff --git a/Meshtastic/Meshtastic.xcdatamodeld/MeshtasticDataModelV 43.xcdatamodel/contents b/Meshtastic/Meshtastic.xcdatamodeld/MeshtasticDataModelV 43.xcdatamodel/contents new file mode 100644 index 000000000..544d44c17 --- /dev/null +++ b/Meshtastic/Meshtastic.xcdatamodeld/MeshtasticDataModelV 43.xcdatamodel/contents @@ -0,0 +1,475 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/Meshtastic/Views/Bluetooth/Connect.swift b/Meshtastic/Views/Bluetooth/Connect.swift index 0048e4309..a7c7d5b8a 100644 --- a/Meshtastic/Views/Bluetooth/Connect.swift +++ b/Meshtastic/Views/Bluetooth/Connect.swift @@ -52,38 +52,80 @@ struct Connect: View { if #available(iOS 17.0, macOS 14.0, *) { TipView(BluetoothConnectionTip(), arrowEdge: .bottom) } - HStack { - VStack(alignment: .center) { - CircleText(text: node?.user?.shortName ?? "?", color: Color(UIColor(hex: UInt32(node?.num ?? 0))), circleSize: 90) - } - .padding(.trailing) - VStack(alignment: .leading) { - if node != nil { - Text(connectedPeripheral.longName).font(.title2) + VStack(alignment: .leading) { + HStack { + VStack(alignment: .center) { + CircleText(text: node?.user?.shortName ?? "?", color: Color(UIColor(hex: UInt32(node?.num ?? 0))), circleSize: 90) + .padding(.trailing, 5) + if node?.latestDeviceMetrics != nil { + BatteryCompact(batteryLevel: node?.latestDeviceMetrics?.batteryLevel ?? 0, font: .caption, iconFont: .callout, color: .accentColor) + .padding(.trailing, 5) + } } - Text("ble.name").font(.callout)+Text(": \(bleManager.connectedPeripheral?.peripheral.name ?? "unknown".localized)") - .font(.callout).foregroundColor(Color.gray) - if node != nil { - Text("firmware.version").font(.callout)+Text(": \(node?.metadata?.firmwareVersion ?? "unknown".localized)") + .padding(.trailing) + VStack(alignment: .leading) { + if node != nil { + Text(connectedPeripheral.longName).font(.title2) + } + Text("ble.name").font(.callout)+Text(": \(bleManager.connectedPeripheral?.peripheral.name ?? "unknown".localized)") .font(.callout).foregroundColor(Color.gray) - } - if bleManager.isSubscribed { - Text("subscribed").font(.callout) - .foregroundColor(.green) - } else { - - HStack { - if #available(iOS 17.0, macOS 14.0, *) { - Image(systemName: "square.stack.3d.down.forward") - .symbolRenderingMode(.multicolor) - .symbolEffect(.variableColor.reversing.cumulative, options: .repeat(20).speed(3)) + if node != nil { + Text("firmware.version").font(.callout)+Text(": \(node?.metadata?.firmwareVersion ?? "unknown".localized)") + .font(.callout).foregroundColor(Color.gray) + } + if bleManager.isSubscribed { + Text("subscribed").font(.callout) + .foregroundColor(.green) + } else { + HStack { + if #available(iOS 17.0, macOS 14.0, *) { + Image(systemName: "square.stack.3d.down.forward") + .symbolRenderingMode(.multicolor) + .symbolEffect(.variableColor.reversing.cumulative, options: .repeat(20).speed(3)) + .foregroundColor(.orange) + } + Text("communicating").font(.callout) .foregroundColor(.orange) } - Text("communicating").font(.callout) - .foregroundColor(.orange) } } } + VStack { + let localStats = node?.telemetries?.filtered(using: NSPredicate(format: "metricsType == 6")).lastObject as? TelemetryEntity + if localStats != nil { + Divider() + if localStats?.numTotalNodes ?? 0 >= 100 { + Text("\(String(format: "Connected: %d nodes online", localStats?.numOnlineNodes ?? 0))") + .font(.callout) + .fontWeight(.medium) + .foregroundStyle(.secondary) + .fixedSize() + } else { + Text("\(String(format: "Connected: %d of %d nodes online", localStats?.numOnlineNodes ?? 0, localStats?.numTotalNodes ?? 0))") + .font(.callout) + .fontWeight(.medium) + .foregroundStyle(.secondary) + .fixedSize() + } + Text("\(String(format: "Ch. Util: %.2f", localStats?.channelUtilization ?? 0))% \(String(format: "Airtime: %.2f", localStats?.airUtilTx ?? 0))%") + .font(.caption) + .fontWeight(.medium) + .foregroundStyle(.secondary) + Text("Packets Sent: \(localStats?.numPacketsTx ?? 0)") + .font(.caption) + .fontWeight(.medium) + .foregroundStyle(.secondary) + Text("Packets Received: \(localStats?.numPacketsRx ?? 0)") + .font(.caption) + .fontWeight(.medium) + .foregroundStyle(.secondary) + Text("Dupe / Bad Packets: \(localStats?.numPacketsRxBad ?? 0)") + .font(.caption) + .fontWeight(.medium) + .foregroundStyle(.secondary) + .fixedSize() + } + } } .font(.caption) .foregroundColor(Color.gray) @@ -327,17 +369,25 @@ struct Connect: View { #if canImport(ActivityKit) func startNodeActivity() { liveActivityStarted = true - let timerSeconds = 60 - let deviceMetrics = node?.telemetries?.filtered(using: NSPredicate(format: "metricsType == 0")) - let mostRecent = deviceMetrics?.lastObject as? TelemetryEntity + // 15 Minutes Local Stats Interval + let timerSeconds = 900 + let localStats = node?.telemetries?.filtered(using: NSPredicate(format: "metricsType == 6")) + let mostRecent = localStats?.lastObject as? TelemetryEntity let activityAttributes = MeshActivityAttributes(nodeNum: Int(node?.num ?? 0), name: node?.user?.longName ?? "unknown") let future = Date(timeIntervalSinceNow: Double(timerSeconds)) + let initialContentState = MeshActivityAttributes.ContentState(uptimeSeconds: UInt32(mostRecent?.uptimeSeconds ?? 0), + channelUtilization: mostRecent?.channelUtilization ?? 0.0, + airtime: mostRecent?.airUtilTx ?? 0.0, + sentPackets: UInt32(mostRecent?.numPacketsTx ?? 0), + receivedPackets: UInt32(mostRecent?.numPacketsRx ?? 0), + badReceivedPackets: UInt32(mostRecent?.numPacketsRxBad ?? 0), + nodesOnline: UInt32(mostRecent?.numOnlineNodes ?? 0), + totalNodes: UInt32(mostRecent?.numTotalNodes ?? 0), + timerRange: Date.now...future) - let initialContentState = MeshActivityAttributes.ContentState(timerRange: Date.now...future, connected: true, channelUtilization: mostRecent?.channelUtilization ?? 0.0, airtime: mostRecent?.airUtilTx ?? 0.0, batteryLevel: UInt32(mostRecent?.batteryLevel ?? 0), nodes: 17, nodesOnline: 9) - - let activityContent = ActivityContent(state: initialContentState, staleDate: Calendar.current.date(byAdding: .minute, value: 2, to: Date())!) + let activityContent = ActivityContent(state: initialContentState, staleDate: Calendar.current.date(byAdding: .minute, value: 15, to: Date())!) do { let myActivity = try Activity.request(attributes: activityAttributes, content: activityContent, diff --git a/Meshtastic/Views/Messages/ChannelMessageList.swift b/Meshtastic/Views/Messages/ChannelMessageList.swift index 91a922c9e..36cc5592f 100644 --- a/Meshtastic/Views/Messages/ChannelMessageList.swift +++ b/Meshtastic/Views/Messages/ChannelMessageList.swift @@ -98,9 +98,6 @@ struct ChannelMessageList: View { Text("\(ackErrorVal?.display ?? "Empty Ack Error")").fixedSize(horizontal: false, vertical: true) .foregroundStyle(ackErrorVal?.color ?? Color.red) .font(.caption2) - } else { - let messageDate = message.timestamp - Text(" \(messageDate.formattedDate(format: MessageText.dateFormatString))").font(.caption2).foregroundColor(.gray) } } } diff --git a/Meshtastic/Views/Messages/UserList.swift b/Meshtastic/Views/Messages/UserList.swift index bf953d5b4..7ba2205cc 100644 --- a/Meshtastic/Views/Messages/UserList.swift +++ b/Meshtastic/Views/Messages/UserList.swift @@ -227,6 +227,9 @@ struct UserList: View { .onChange(of: maxDistance) { _ in searchUserList() } + .onReceive(users.publisher) { _ in + searchUserList() + } .onAppear { searchUserList() } @@ -307,7 +310,7 @@ struct UserList: View { } /// Online if isOnline { - let isOnlinePredicate = NSPredicate(format: "userNode.lastHeard >= %@", Calendar.current.date(byAdding: .minute, value: -15, to: Date())! as NSDate) + let isOnlinePredicate = NSPredicate(format: "userNode.lastHeard >= %@", Calendar.current.date(byAdding: .minute, value: -120, to: Date())! as NSDate) predicates.append(isOnlinePredicate) } /// Encrypted diff --git a/Meshtastic/Views/Nodes/MeshMap.swift b/Meshtastic/Views/Nodes/MeshMap.swift index 9e09a0c07..f471caaf7 100644 --- a/Meshtastic/Views/Nodes/MeshMap.swift +++ b/Meshtastic/Views/Nodes/MeshMap.swift @@ -126,7 +126,7 @@ struct MeshMap: View { guard case .map(let selectedNodeNum) = router.navigationState else { return } // TODO: handle deep link for waypoints } - .onChange(of: (selectedMapLayer)) { newMapLayer in + .onChange(of: selectedMapLayer) { newMapLayer in switch selectedMapLayer { case .standard: UserDefaults.mapLayer = newMapLayer diff --git a/Meshtastic/Views/Nodes/NodeList.swift b/Meshtastic/Views/Nodes/NodeList.swift index 676363281..c0f16dd85 100644 --- a/Meshtastic/Views/Nodes/NodeList.swift +++ b/Meshtastic/Views/Nodes/NodeList.swift @@ -344,6 +344,11 @@ struct NodeList: View { self.selectedNode = nil } } + .onReceive(nodes.publisher) { _ in + Task { + await searchNodeList() + } + } .onAppear { Task { await searchNodeList() @@ -390,7 +395,7 @@ struct NodeList: View { } /// Online if isOnline { - let isOnlinePredicate = NSPredicate(format: "lastHeard >= %@", Calendar.current.date(byAdding: .minute, value: -15, to: Date())! as NSDate) + let isOnlinePredicate = NSPredicate(format: "lastHeard >= %@", Calendar.current.date(byAdding: .minute, value: -120, to: Date())! as NSDate) predicates.append(isOnlinePredicate) } /// Encrypted diff --git a/Meshtastic/Views/Settings/AppSettings.swift b/Meshtastic/Views/Settings/AppSettings.swift index 022866556..f641b77ba 100644 --- a/Meshtastic/Views/Settings/AppSettings.swift +++ b/Meshtastic/Views/Settings/AppSettings.swift @@ -76,9 +76,14 @@ struct AppSettings: View { } clearCoreDataDatabase(context: context, includeRoutes: true) context.refreshAllObjects() - UserDefaults.standard.reset() } } + Button { + UserDefaults.standard.reset() + } label: { + Label("Reset App Settings", systemImage: "arrow.counterclockwise.circle") + .foregroundColor(.red) + } } if totalDownloadedTileSize != "0MB" { Section(header: Text("Map Tile Data")) { diff --git a/Meshtastic/Views/Settings/Channels.swift b/Meshtastic/Views/Settings/Channels.swift index ce48b6589..61cddceac 100644 --- a/Meshtastic/Views/Settings/Channels.swift +++ b/Meshtastic/Views/Settings/Channels.swift @@ -92,13 +92,22 @@ struct Channels: View { positionPrecision = 32 preciseLocation = true positionsEnabled = true - + if channelKey == "AQ==" { + positionPrecision = 13 + preciseLocation = false + positionsEnabled = true + } } else if !supportedVersion && channelRole == 2 { positionPrecision = 0 preciseLocation = false positionsEnabled = false } else { - if positionPrecision == 32 { + if channelKey == "AQ==" { + preciseLocation = false + if positionPrecision < 10 || positionPrecision > 16 { + positionPrecision = 13 + } + } else if positionPrecision == 32 { preciseLocation = true positionsEnabled = true } else { @@ -250,7 +259,7 @@ struct Channels: View { channelKey = key positionsEnabled = false preciseLocation = false - positionPrecision = 0 + positionPrecision = 13 uplink = false downlink = false hasChanges = true diff --git a/Meshtastic/Views/Settings/Channels/ChannelForm.swift b/Meshtastic/Views/Settings/Channels/ChannelForm.swift index 780a19b47..52e1f59e7 100644 --- a/Meshtastic/Views/Settings/Channels/ChannelForm.swift +++ b/Meshtastic/Views/Settings/Channels/ChannelForm.swift @@ -157,7 +157,8 @@ struct ChannelForm: View { if !preciseLocation { VStack(alignment: .leading) { Label("Approximate Location", systemImage: "location.slash.circle.fill") - Slider(value: $positionPrecision, in: 10...19, step: 1) { + + Slider(value: $positionPrecision, in: 10...16, step: 1) { } minimumValueLabel: { Image(systemName: "minus") } maximumValueLabel: { @@ -199,6 +200,12 @@ struct ChannelForm: View { .onChange(of: channelKey) { _ in hasChanges = true } + .onChange(of: channelKeySize) { _ in + if channelKeySize == -1 { + preciseLocation = false + channelKey = "AQ==" + } + } .onChange(of: channelRole) { _ in hasChanges = true } @@ -206,7 +213,7 @@ struct ChannelForm: View { if loc == true { positionPrecision = 32 } else { - positionPrecision = 14 + positionPrecision = 13 } hasChanges = true } @@ -216,7 +223,7 @@ struct ChannelForm: View { .onChange(of: positionsEnabled) { pe in if pe { if positionPrecision == 0 { - positionPrecision = 32 + positionPrecision = 13 } } else { positionPrecision = 0 @@ -229,7 +236,7 @@ struct ChannelForm: View { .onChange(of: downlink) { _ in hasChanges = true } - .onAppear { + .onFirstAppear { let tempKey = Data(base64Encoded: channelKey) ?? Data() if tempKey.count == channelKeySize || channelKeySize == -1 { hasValidKey = true diff --git a/Meshtastic/Views/Settings/Config/Module/MQTTConfig.swift b/Meshtastic/Views/Settings/Config/Module/MQTTConfig.swift index d6176526e..8ef411ea6 100644 --- a/Meshtastic/Views/Settings/Config/Module/MQTTConfig.swift +++ b/Meshtastic/Views/Settings/Config/Module/MQTTConfig.swift @@ -24,7 +24,7 @@ struct MQTTConfig: View { @State var password = "" @State var encryptionEnabled = true @State var jsonEnabled = false - @State var tlsEnabled = true + @State var tlsEnabled = false @State var root = "msh" @State var selectedTopic = "" @State var mqttConnected: Bool = false @@ -234,7 +234,7 @@ struct MQTTConfig: View { .listRowSeparator(/*@START_MENU_TOKEN@*/.visible/*@END_MENU_TOKEN@*/) Toggle(isOn: $tlsEnabled) { Label("TLS Enabled", systemImage: "checkmark.shield.fill") - Text("Your MQTT Server must support TLS.") + Text("Your MQTT Server must support TLS. Not available via the public mqtt server.") } .toggleStyle(SwitchToggleStyle(tint: .accentColor)) } @@ -288,9 +288,6 @@ struct MQTTConfig: View { jsonEnabled = false } if newProxyToClientEnabled != node?.mqttConfig?.proxyToClientEnabled { hasChanges = true } - if newProxyToClientEnabled { - jsonEnabled = false - } } .onChange(of: address) { newAddress in if node != nil && node?.mqttConfig != nil { @@ -324,8 +321,12 @@ struct MQTTConfig: View { } if newJsonEnabled != node?.mqttConfig?.jsonEnabled { hasChanges = true } } - .onChange(of: tlsEnabled) { - if $0 != node?.mqttConfig?.tlsEnabled { hasChanges = true } + .onChange(of: tlsEnabled) { newTlsEnabled in + if address.lowercased() == "mqtt.meshtastic.org" { + tlsEnabled = false + } else { + if newTlsEnabled != node?.mqttConfig?.tlsEnabled { hasChanges = true } + } } .onChange(of: mqttConnected) { newMqttConnected in if newMqttConnected == false { diff --git a/Widgets/MeshActivityAttributes.swift b/Widgets/MeshActivityAttributes.swift index 916377c94..a2abdbba4 100644 --- a/Widgets/MeshActivityAttributes.swift +++ b/Widgets/MeshActivityAttributes.swift @@ -15,13 +15,15 @@ struct MeshActivityAttributes: ActivityAttributes { public typealias MeshActivityStatus = ContentState public struct ContentState: Codable, Hashable { // Dynamic stateful properties about your activity go here! - var timerRange: ClosedRange - var connected: Bool + var uptimeSeconds: UInt32 var channelUtilization: Float var airtime: Float - var batteryLevel: UInt32 - var nodes: Int - var nodesOnline: Int + var sentPackets: UInt32 + var receivedPackets: UInt32 + var badReceivedPackets: UInt32 + var nodesOnline: UInt32 + var totalNodes: UInt32 + var timerRange: ClosedRange } // Fixed non-changing properties about your activity go here! diff --git a/Widgets/WidgetsLiveActivity.swift b/Widgets/WidgetsLiveActivity.swift index 396aaac9b..14d9762f5 100644 --- a/Widgets/WidgetsLiveActivity.swift +++ b/Widgets/WidgetsLiveActivity.swift @@ -13,64 +13,84 @@ struct WidgetsLiveActivity: Widget { var body: some WidgetConfiguration { ActivityConfiguration(for: MeshActivityAttributes.self) { context in - LiveActivityView(nodeName: context.attributes.name, channelUtilization: context.state.channelUtilization, airtime: context.state.airtime, batteryLevel: context.state.batteryLevel, nodes: 17, nodesOnline: 7, timerRange: context.state.timerRange) - .widgetURL(URL(string: "meshtastic:///node/\(context.attributes.name)")) + LiveActivityView(nodeName: context.attributes.name, + uptimeSeconds: 0, // context.attributes.uptimeSeconds, + channelUtilization: context.state.channelUtilization, + airtime: context.state.airtime, + sentPackets: context.state.sentPackets, + receivedPackets: context.state.receivedPackets, + badReceivedPackets: context.state.badReceivedPackets, + nodesOnline: context.state.nodesOnline, + totalNodes: context.state.totalNodes, + timerRange: context.state.timerRange) + .widgetURL(URL(string: "meshtastic:///bluetooth")) } dynamicIsland: { context in DynamicIsland { DynamicIslandExpandedRegion(.leading) { - Text("Network") - .font(.headline) - .fontWeight(.bold) - .foregroundStyle(.secondary) - .fixedSize() - .padding(.top, 10) + HStack(alignment: /*@START_MENU_TOKEN@*/.center/*@END_MENU_TOKEN@*/) { + Spacer() + Text("Mesh") + .font(.callout) + .fontWeight(.medium) + .foregroundStyle(.primary) + .padding(.bottom, 10) + .fixedSize() + Spacer() + } + if context.state.nodesOnline >= 100 { + Text("100+ online") + .font(.caption) + .foregroundStyle(.secondary) + .fixedSize() + } else { + Text("\(context.state.nodesOnline) of \(context.state.totalNodes) online") + .font(.caption) + .foregroundStyle(.secondary) + .fixedSize() + } Text("\(String(format: "Ch. Util: %.2f", context.state.channelUtilization))%") - .font(.headline) - .fontWeight(.medium) + .font(.caption) .foregroundStyle(.secondary) .fixedSize() Text("\(String(format: "Airtime: %.2f", context.state.airtime))%") - .font(.headline) - .fontWeight(.medium) + .font(.caption) .foregroundStyle(.secondary) .fixedSize() - Spacer() } DynamicIslandExpandedRegion(.center) { - VStack(alignment: .center, spacing: 0) { - BatteryIcon(batteryLevel: Int32(context.state.batteryLevel), font: .title, color: .accentColor) - if context.state.batteryLevel == 0 { - Text("< 1%") - .font(.title3) - .foregroundColor(.gray) - .fixedSize() - } else if context.state.batteryLevel < 101 { - Text(String(context.state.batteryLevel) + "%") - .font(.title3) - .foregroundColor(.gray) - .fixedSize() - } else { - Text("PWD") - .font(.title3) - .foregroundColor(.gray) - } - } - } - DynamicIslandExpandedRegion(.trailing, priority: 1) { TimerView(timerRange: context.state.timerRange) .tint(Color("LightIndigo")) - + } + DynamicIslandExpandedRegion(.trailing, priority: 1) { + HStack(alignment: /*@START_MENU_TOKEN@*/.center/*@END_MENU_TOKEN@*/) { + Spacer() + Text("Packets") + .font(.callout) + .fontWeight(.medium) + .foregroundStyle(.primary) + .padding(.bottom, 10) + .fixedSize() + Spacer() + } + Text("Sent: \(context.state.sentPackets)") + .font(.caption) + .foregroundStyle(.secondary) + .fixedSize() + Text("Received: \(context.state.receivedPackets)") + .font(.caption) + .foregroundStyle(.secondary) + .fixedSize() + Text("Dupe / Bad \(context.state.badReceivedPackets)") + .font(.caption) + .foregroundStyle(.secondary) + .fixedSize() } DynamicIslandExpandedRegion(.bottom) { - Text(context.attributes.name) - .font(context.attributes.name.count > 14 ? .callout : .title3) - .fontWeight(.semibold) - .foregroundStyle(.tint) Text("Last Heard: \(Date().formatted())") .font(.caption) .fontWeight(.medium) - .foregroundStyle(.secondary) + .foregroundStyle(.tint) .fixedSize() } @@ -95,85 +115,63 @@ struct WidgetsLiveActivity: Widget { .contentMargins(.trailing, 32, for: .expanded) .contentMargins([.leading, .top, .bottom], 6, for: .compactLeading) .contentMargins(.all, 6, for: .minimal) - .widgetURL(URL(string: "meshtastic:///node/\(context.attributes.name)")) + .widgetURL(URL(string: "meshtastic:///bluetooth")) } } } -struct WidgetsLiveActivity_Previews: PreviewProvider { - static let attributes = MeshActivityAttributes(nodeNum: 123456789, name: "RAK Compact Rotary Handset Gray 8E6G") - static let state = MeshActivityAttributes.ContentState( - timerRange: Date.now...Date(timeIntervalSinceNow: 60), connected: true, channelUtilization: 25.84, airtime: 10.01, batteryLevel: 39, nodes: 17, nodesOnline: 9) - - static var previews: some View { - attributes - .previewContext(state, viewKind: .dynamicIsland(.compact)) - .previewDisplayName("Compact") - attributes - .previewContext(state, viewKind: .dynamicIsland(.minimal)) - .previewDisplayName("Minimal") - attributes - .previewContext(state, viewKind: .dynamicIsland(.expanded)) - .previewDisplayName("Expanded") - attributes - .previewContext(state, viewKind: .content) - .previewDisplayName("Notification") - } -} +//struct WidgetsLiveActivity_Previews: PreviewProvider { +// static let attributes = MeshActivityAttributes(nodeNum: 123456789, name: "RAK Compact Rotary Handset Gray 8E6G") +// static let state = MeshActivityAttributes.ContentState( +// timerRange: Date.now...Date(timeIntervalSinceNow: 60), connected: true, channelUtilization: 25.84, airtime: 10.01, batteryLevel: 39, nodes: 17, nodesOnline: 9) +// +// static var previews: some View { +// attributes +// .previewContext(state, viewKind: .dynamicIsland(.compact)) +// .previewDisplayName("Compact") +// attributes +// .previewContext(state, viewKind: .dynamicIsland(.minimal)) +// .previewDisplayName("Minimal") +// attributes +// .previewContext(state, viewKind: .dynamicIsland(.expanded)) +// .previewDisplayName("Expanded") +// attributes +// .previewContext(state, viewKind: .content) +// .previewDisplayName("Notification") +// } +//} struct LiveActivityView: View { @Environment(\.colorScheme) private var colorScheme @Environment(\.isLuminanceReduced) var isLuminanceReduced var nodeName: String - // var connected: Bool + var uptimeSeconds: UInt32 var channelUtilization: Float var airtime: Float - var batteryLevel: UInt32 - var nodes: Int - var nodesOnline: Int + var sentPackets: UInt32 + var receivedPackets: UInt32 + var badReceivedPackets: UInt32 + var nodesOnline: UInt32 + var totalNodes: UInt32 var timerRange: ClosedRange var body: some View { HStack { + Spacer() Image(colorScheme == .light ? "m-logo-black" : "m-logo-white") .resizable() .clipShape(ContainerRelativeShape()) .opacity(isLuminanceReduced ? 0.5 : 1.0) .aspectRatio(contentMode: .fit) - .frame(width: 65) + .frame(minWidth: 25, idealWidth: 45, maxWidth: 55) Spacer() - NodeInfoView(nodeName: nodeName, timerRange: timerRange, channelUtilization: channelUtilization, airtime: airtime, batteryLevel: batteryLevel, nodes: nodes, nodesOnline: nodesOnline) + NodeInfoView(isLuminanceReduced: _isLuminanceReduced, nodeName: nodeName, uptimeSeconds: uptimeSeconds, channelUtilization: channelUtilization, airtime: airtime, sentPackets: sentPackets, receivedPackets: receivedPackets, badReceivedPackets: badReceivedPackets, nodesOnline: nodesOnline, totalNodes: totalNodes, timerRange: timerRange) Spacer() - VStack { - BatteryIcon(batteryLevel: Int32(batteryLevel), font: .title, color: .secondary) - if batteryLevel == 0 { - Text("< 1%") - .font(.headline) - .fontWeight(.medium) - .foregroundStyle(.secondary) - .opacity(isLuminanceReduced ? 0.8 : 1.0) - .fixedSize() - } else if batteryLevel < 101 { - Text(String(batteryLevel) + "%") - .font(.headline) - .fontWeight(.medium) - .foregroundStyle(.secondary) - .opacity(isLuminanceReduced ? 0.8 : 1.0) - .fixedSize() - } else { - Text("Plugged In") - .font(.headline) - .fontWeight(.medium) - .foregroundStyle(.secondary) - .opacity(isLuminanceReduced ? 0.8 : 1.0) - .fixedSize() - } - } } .tint(.primary) .padding([.leading, .top, .bottom]) - .padding(.trailing, 32) + .padding(.trailing, 25) .activityBackgroundTint(colorScheme == .light ? Color("LiveActivityBackground") : Color("AccentColorDimmed")) .activitySystemActionForegroundColor(.primary) } @@ -183,12 +181,15 @@ struct NodeInfoView: View { @Environment(\.isLuminanceReduced) var isLuminanceReduced var nodeName: String - var timerRange: ClosedRange + var uptimeSeconds: UInt32 var channelUtilization: Float var airtime: Float - var batteryLevel: UInt32 - var nodes: Int - var nodesOnline: Int + var sentPackets: UInt32 + var receivedPackets: UInt32 + var badReceivedPackets: UInt32 + var nodesOnline: UInt32 + var totalNodes: UInt32 + var timerRange: ClosedRange var body: some View { VStack(alignment: .leading, spacing: 0) { @@ -196,24 +197,45 @@ struct NodeInfoView: View { .font(nodeName.count > 14 ? .callout : .title3) .fontWeight(.semibold) .foregroundStyle(.tint) - Text("\(String(format: "Ch. Util: %.2f", channelUtilization))%") - .font(.headline) + Text("\(String(format: "Ch. Util: %.2f", channelUtilization))% \(String(format: "Airtime: %.2f", airtime))%") + .font(.caption) + .fontWeight(.medium) + .foregroundStyle(.secondary) + .opacity(isLuminanceReduced ? 0.8 : 1.0) + .fixedSize() + Text("Packets Sent: \(sentPackets)") + .font(.caption) .fontWeight(.medium) .foregroundStyle(.secondary) .opacity(isLuminanceReduced ? 0.8 : 1.0) .fixedSize() - Text("\(String(format: "Airtime: %.2f", airtime))%") - .font(.headline) + Text("Packets Received: \(receivedPackets)") + .font(.caption) + .fontWeight(.medium) + .foregroundStyle(.secondary) + .opacity(isLuminanceReduced ? 0.8 : 1.0) + .fixedSize() + Text("Dupe / Bad Packets: \(badReceivedPackets)") + .font(.caption) .fontWeight(.medium) .foregroundStyle(.secondary) .opacity(isLuminanceReduced ? 0.8 : 1.0) .fixedSize() -// Text("\(String(format: "Connected: %d of %d online", nodesOnline, nodes))") -// .font(.callout) -// .fontWeight(.medium) -// .foregroundStyle(.secondary) -// .opacity(isLuminanceReduced ? 0.8 : 1.0) -// .fixedSize() + if totalNodes >= 100 { + Text("\(String(format: "Connected: %d nodes online", nodesOnline))") + .font(.caption) + .fontWeight(.medium) + .foregroundStyle(.secondary) + .opacity(isLuminanceReduced ? 0.8 : 1.0) + .fixedSize() + } else { + Text("\(String(format: "Connected: %d of %d nodes online", nodesOnline, totalNodes))") + .font(.caption) + .fontWeight(.medium) + .foregroundStyle(.secondary) + .opacity(isLuminanceReduced ? 0.8 : 1.0) + .fixedSize() + } let now = Date() Text("Last Heard: \(now.formatted())") .font(.caption) @@ -255,8 +277,9 @@ struct TimerView: View { var body: some View { VStack(alignment: .center) { - Text("NEXT UPDATE") - .font(.caption) + Text("UPDATE IN") + .font(.caption2) + .allowsTightening(/*@START_MENU_TOKEN@*/true/*@END_MENU_TOKEN@*/) .fontWeight(.medium) .foregroundStyle(.secondary) .opacity(isLuminanceReduced ? 0.5 : 1.0) @@ -268,10 +291,12 @@ struct TimerView: View { .fontWeight(.semibold) .foregroundStyle(.tint) Image(systemName: "timer") + .symbolRenderingMode(.multicolor) .resizable() .foregroundStyle(.secondary) .frame(width: 30, height: 30) .opacity(isLuminanceReduced ? 0.5 : 1.0) + .offset(y: -5) } } }