diff --git a/Meshtastic/Helpers/Extensions.swift b/Meshtastic/Helpers/Extensions.swift index 9345d7ea..45705463 100644 --- a/Meshtastic/Helpers/Extensions.swift +++ b/Meshtastic/Helpers/Extensions.swift @@ -20,6 +20,22 @@ extension CLLocationCoordinate2D { } } +extension Color { + func isLight() -> Bool { + guard let components = cgColor?.components, components.count > 2 else {return false} + let brightness = ((components[0] * 299) + (components[1] * 587) + (components[2] * 114)) / 1000 + return (brightness > 0.5) + } +} + +extension UIColor { + func isLight() -> Bool { + guard let components = cgColor.components, components.count > 2 else {return false} + let brightness = ((components[0] * 299) + (components[1] * 587) + (components[2] * 114)) / 1000 + return (brightness > 0.5) + } +} + extension Data { var macAddressString: String { let mac: String = reduce("") {$0 + String(format: "%02x:", $1)} @@ -72,6 +88,21 @@ extension Int { } } + + +extension Int64 { + + func uiColor() -> UIColor { + let color = UIColor( + red: CGFloat((self & 0xFF0000) >> 16) / 255.0, + green: CGFloat((self & 0x00FF00) >> 8) / 255.0, + blue: CGFloat(self & 0x0000FF) / 255.0, + alpha: CGFloat(1.0)) + + return color + } +} + extension UIImage { func rotate(radians: Float) -> UIImage? { var newSize = CGRect(origin: CGPoint.zero, size: self.size).applying(CGAffineTransform(rotationAngle: CGFloat(radians))).size diff --git a/Meshtastic/Helpers/MeshPackets.swift b/Meshtastic/Helpers/MeshPackets.swift index 597d88d8..4579033e 100644 --- a/Meshtastic/Helpers/MeshPackets.swift +++ b/Meshtastic/Helpers/MeshPackets.swift @@ -766,23 +766,25 @@ func textMessageAppPacket(packet: MeshPacket, connectedNode: Int64, context: NSM guard let fetchedMyInfo = try context.fetch(fetchMyInfoRequest) as? [MyInfoEntity] else { return } - for channel in (fetchedMyInfo[0].channels?.array ?? []) as? [ChannelEntity] ?? [] { - if channel.index == newMessage.channel { - context.refresh(channel, mergeChanges: true) - } - - if channel.index == newMessage.channel && !channel.mute { - // Create an iOS Notification for the received private channel message and schedule it immediately - let manager = LocalNotificationManager() - manager.notifications = [ - Notification( - id: ("notification.id.\(newMessage.messageId)"), - title: "\(newMessage.fromUser?.longName ?? NSLocalizedString("unknown", comment: "Unknown"))", - subtitle: "AKA \(newMessage.fromUser?.shortName ?? "???")", - content: messageText) - ] - manager.schedule() - print("💬 iOS Notification Scheduled for text message from \(newMessage.fromUser?.longName ?? NSLocalizedString("unknown", comment: "Unknown"))") + if !fetchedMyInfo.isEmpty { + for channel in (fetchedMyInfo[0].channels?.array ?? []) as? [ChannelEntity] ?? [] { + if channel.index == newMessage.channel { + context.refresh(channel, mergeChanges: true) + } + + if channel.index == newMessage.channel && !channel.mute { + // Create an iOS Notification for the received private channel message and schedule it immediately + let manager = LocalNotificationManager() + manager.notifications = [ + Notification( + id: ("notification.id.\(newMessage.messageId)"), + title: "\(newMessage.fromUser?.longName ?? NSLocalizedString("unknown", comment: "Unknown"))", + subtitle: "AKA \(newMessage.fromUser?.shortName ?? "???")", + content: messageText) + ] + manager.schedule() + print("💬 iOS Notification Scheduled for text message from \(newMessage.fromUser?.longName ?? NSLocalizedString("unknown", comment: "Unknown"))") + } } } } catch { diff --git a/Meshtastic/Views/Helpers/CircleText.swift b/Meshtastic/Views/Helpers/CircleText.swift index b4b51515..30d40406 100644 --- a/Meshtastic/Views/Helpers/CircleText.swift +++ b/Meshtastic/Views/Helpers/CircleText.swift @@ -11,6 +11,7 @@ struct CircleText: View { var circleSize: CGFloat? = 60 var fontSize: CGFloat? = 20 var brightness: Double? = 0 + var textColor: Color? = .white var body: some View { @@ -21,7 +22,7 @@ struct CircleText: View { .fill(color) .brightness(brightness ?? 0) .frame(width: circleSize, height: circleSize) - Text(text).textCase(.uppercase).font(font).foregroundColor(.white).fixedSize() + Text(text).textCase(.uppercase).font(font).foregroundColor(textColor).fixedSize() .frame(width: circleSize, height: circleSize, alignment: /*@START_MENU_TOKEN@*/.center/*@END_MENU_TOKEN@*/).offset(x: 0, y: 0) } } diff --git a/Meshtastic/Views/Map/Custom/MapViewSwiftUI.swift b/Meshtastic/Views/Map/Custom/MapViewSwiftUI.swift index 86f8c0a8..a32e606c 100644 --- a/Meshtastic/Views/Map/Custom/MapViewSwiftUI.swift +++ b/Meshtastic/Views/Map/Custom/MapViewSwiftUI.swift @@ -16,8 +16,6 @@ struct MapViewSwiftUI: UIViewRepresentable { var onLongPress: (_ waypointCoordinate: CLLocationCoordinate2D) -> Void var onWaypointEdit: (_ waypointId: Int ) -> Void let mapView = MKMapView() - let lineColors: [UIColor] = [UIColor.systemIndigo, UIColor.yellow, UIColor.white, UIColor.red, UIColor.purple, UIColor.orange, UIColor.magenta, UIColor.lightGray, UIColor.green, UIColor.gray, UIColor.systemMint, UIColor.darkGray, UIColor.cyan, UIColor.brown, UIColor.blue, UIColor.black, UIColor.systemPink, - UIColor.systemTeal] // Parameters let positions: [PositionEntity] let waypoints: [WaypointEntity] @@ -142,7 +140,7 @@ struct MapViewSwiftUI: UIViewRepresentable { return position.nodeCoordinate! }) let polyline = MKPolyline(coordinates: lineCoords, count: nodePositions.count) - polyline.title = "\(String(position.nodePosition?.num ?? 0))-\(String(lineIndex))" + polyline.title = "\(String(position.nodePosition?.num ?? 0))" mapView.addOverlay(polyline) lineIndex += 1 // There are 18 colors for lines, start over if we are at index 17 @@ -199,7 +197,7 @@ struct MapViewSwiftUI: UIViewRepresentable { annotationView.displayPriority = .required annotationView.titleVisibility = .visible } else { - annotationView.markerTintColor = UIColor(.indigo) + annotationView.markerTintColor = positionAnnotation.nodePosition?.num.uiColor() annotationView.displayPriority = .defaultHigh annotationView.titleVisibility = .adaptive } @@ -351,10 +349,10 @@ struct MapViewSwiftUI: UIViewRepresentable { } else { if let routePolyline = overlay as? MKPolyline { - let titleString = routePolyline.title ?? "None-0" + let titleString = routePolyline.title ?? "0" let index = Int(titleString.components(separatedBy: "-").last ?? "0") let renderer = MKPolylineRenderer(polyline: routePolyline) - renderer.strokeColor = parent.lineColors[index ?? 0] + renderer.strokeColor = Int64(titleString)?.uiColor() renderer.lineWidth = 8 return renderer } diff --git a/Meshtastic/Views/Messages/Contacts.swift b/Meshtastic/Views/Messages/Contacts.swift index 3a193e4f..a2e1dba2 100644 --- a/Meshtastic/Views/Messages/Contacts.swift +++ b/Meshtastic/Views/Messages/Contacts.swift @@ -150,7 +150,7 @@ struct Contacts: View { HStack { VStack { HStack { - CircleText(text: user.shortName ?? "???", color: .accentColor, circleSize: 52, fontSize: 16, brightness: 0.1) + CircleText(text: user.shortName ?? "???", color: Color(user.num.uiColor()), circleSize: 52, fontSize: 16, textColor: user.num.uiColor().isLight() ? .black : .white) .padding(.trailing, 5) VStack { HStack { diff --git a/Meshtastic/Views/Nodes/DeviceMetricsLog.swift b/Meshtastic/Views/Nodes/DeviceMetricsLog.swift index 96a4e87e..9bf5bed7 100644 --- a/Meshtastic/Views/Nodes/DeviceMetricsLog.swift +++ b/Meshtastic/Views/Nodes/DeviceMetricsLog.swift @@ -21,25 +21,67 @@ struct DeviceMetricsLog: View { let oneDayAgo = Calendar.current.date(byAdding: .day, value: -3, to: Date()) let deviceMetrics = node.telemetries?.filtered(using: NSPredicate(format: "metricsType == 0")).reversed() as? [TelemetryEntity] ?? [] - let batteryChartData = deviceMetrics + let chartData = deviceMetrics .filter { $0.time != nil && $0.time! >= oneDayAgo! } .sorted { $0.time! < $1.time! } NavigationStack { - if batteryChartData.count > 0 { + if chartData.count > 0 { + GroupBox(label: Label("battery.level.trend", systemImage: "battery.100")) { - Chart(batteryChartData, id: \.self) { - LineMark( - x: .value("Hour", $0.time!.formattedDate(format: "ha")), - y: .value("Value", $0.batteryLevel) - ) + + Chart(chartData, id: \.self) { + PointMark( - x: .value("Hour", $0.time!.formattedDate(format: "ha")), + x: .value("Time", $0.time!, unit: .hour), + y: .value("Value", $0.channelUtilization) + ) + .foregroundStyle(.green) + + LineMark( + x: .value("Time", $0.time!, unit: .hour), + y: .value("Value", $0.channelUtilization) + ) + .foregroundStyle(.green) + .interpolationMethod(.catmullRom) + + PointMark( + x: .value("Time", $0.time!, unit: .hour), y: .value("Value", $0.batteryLevel) ) + .foregroundStyle(.blue) + + LineMark( + x: .value("Time", $0.time!, unit: .hour), + y: .value("Value", $0.batteryLevel) + ) + .foregroundStyle(.blue) + + PointMark( + //x: .value("Hour", $0.time!.formattedDate(format: "ha")), + x: .value("Time", $0.time!, unit: .hour), + y: .value("Value", $0.airUtilTx) + ) + .foregroundStyle(.red) + + LineMark( + x: .value("Time", $0.time!, unit: .hour), + y: .value("Value", $0.airUtilTx) + ) + .foregroundStyle(.red) } - .frame(height: 150) + // Set color for each data in the chart + .chartForegroundStyleScale([ + "Battery Level" : .blue, + "Channel Utilization": .green, + "Airtime": .red + ]) + .chartLegend(position: .automatic, alignment: .bottom) + .chartXAxis { + AxisMarks(values: .stride(by: .hour)) + } + //.frame(height: 200) } } let localeDateFormat = DateFormatter.dateFormat(fromTemplate: "yyMMddjmma", options: 0, locale: Locale.current) diff --git a/Meshtastic/Views/Nodes/EnvironmentMetricsLog.swift b/Meshtastic/Views/Nodes/EnvironmentMetricsLog.swift index 46a16dd8..e9655414 100644 --- a/Meshtastic/Views/Nodes/EnvironmentMetricsLog.swift +++ b/Meshtastic/Views/Nodes/EnvironmentMetricsLog.swift @@ -19,47 +19,36 @@ struct EnvironmentMetricsLog: View { var node: NodeInfoEntity var body: some View { + + let environmentMetrics = node.telemetries?.filtered(using: NSPredicate(format: "metricsType == 1")).reversed() as? [TelemetryEntity] ?? [] NavigationStack { let localeDateFormat = DateFormatter.dateFormat(fromTemplate: "yyMMddjmma", options: 0, locale: Locale.current) let dateFormatString = (localeDateFormat ?? "MM/dd/YY j:mma").replacingOccurrences(of: ",", with: "") + Text("\(environmentMetrics.count) Readings") if UIDevice.current.userInterfaceIdiom == .pad || UIDevice.current.userInterfaceIdiom == .mac { // Add a table for mac and ipad - Table(node.telemetries!.reversed() as! [TelemetryEntity]) { + Table(environmentMetrics) { TableColumn("Temperature") { em in - if em.metricsType == 1 { - Text(em.temperature.formattedTemperature()) - } + Text(em.temperature.formattedTemperature()) } TableColumn("Humidity") { em in - if em.metricsType == 1 { - Text("\(String(format: "%.2f", em.relativeHumidity))%") - } + Text("\(String(format: "%.2f", em.relativeHumidity))%") } TableColumn("Barometric Pressure") { em in - if em.metricsType == 1 { - Text("\(String(format: "%.2f", em.barometricPressure)) hPa") - } + Text("\(String(format: "%.2f", em.barometricPressure)) hPa") } TableColumn("gas.resistance") { em in - if em.metricsType == 1 { - Text("\(String(format: "%.2f", em.gasResistance)) ohms") - } + Text("\(String(format: "%.2f", em.gasResistance)) ohms") } TableColumn("current") { em in - if em.metricsType == 1 { - Text("\(String(format: "%.2f", em.current))") - } + Text("\(String(format: "%.2f", em.current))") } TableColumn("voltage") { em in - if em.metricsType == 1 { - Text("\(String(format: "%.2f", em.voltage))") - } + Text("\(String(format: "%.2f", em.voltage))") } TableColumn("timestamp") { em in - if em.metricsType == 1 { - Text(em.time?.formattedDate(format: dateFormatString) ?? NSLocalizedString("unknown.age", comment: "")) - } + Text(em.time?.formattedDate(format: dateFormatString) ?? NSLocalizedString("unknown.age", comment: "")) } } } else { @@ -92,21 +81,18 @@ struct EnvironmentMetricsLog: View { } ForEach(node.telemetries?.reversed() as? [TelemetryEntity] ?? [], id: \.self) { (em: TelemetryEntity) in - if em.metricsType == 1 { + GridRow { - GridRow { - - Text(em.temperature.formattedTemperature()) - .font(.caption) - Text("\(String(format: "%.2f", em.relativeHumidity))%") - .font(.caption) - Text("\(String(format: "%.2f", em.barometricPressure))") - .font(.caption) - Text("\(String(format: "%.2f", em.gasResistance))") - .font(.caption) - Text(em.time?.formattedDate(format: dateFormatString) ?? NSLocalizedString("unknown.age", comment: "")) - .font(.caption2) - } + Text(em.temperature.formattedTemperature()) + .font(.caption) + Text("\(String(format: "%.2f", em.relativeHumidity))%") + .font(.caption) + Text("\(String(format: "%.2f", em.barometricPressure))") + .font(.caption) + Text("\(String(format: "%.2f", em.gasResistance))") + .font(.caption) + Text(em.time?.formattedDate(format: dateFormatString) ?? NSLocalizedString("unknown.age", comment: "")) + .font(.caption2) } } } diff --git a/Meshtastic/Views/Nodes/NodeDetail.swift b/Meshtastic/Views/Nodes/NodeDetail.swift index 24634d6c..b80f4be1 100644 --- a/Meshtastic/Views/Nodes/NodeDetail.swift +++ b/Meshtastic/Views/Nodes/NodeDetail.swift @@ -145,7 +145,7 @@ struct NodeDetail: View { if UIDevice.current.userInterfaceIdiom == .pad || UIDevice.current.userInterfaceIdiom == .mac { HStack { VStack(alignment: .center) { - CircleText(text: node.user?.shortName ?? "???", color: .accentColor, circleSize: 75, fontSize: 26) + CircleText(text: node.user?.shortName ?? "???", color: Color(node.num.uiColor()), circleSize: 75, fontSize: 26, textColor: node.num.uiColor().isLight() ? .black : .white ) } Divider() VStack { diff --git a/Meshtastic/Views/Nodes/NodeList.swift b/Meshtastic/Views/Nodes/NodeList.swift index 983b9846..043140e9 100644 --- a/Meshtastic/Views/Nodes/NodeList.swift +++ b/Meshtastic/Views/Nodes/NodeList.swift @@ -26,9 +26,11 @@ struct NodeList: View { @State private var selection: NodeInfoEntity? // Nothing selected by default. var body: some View { + + NavigationSplitView { - List(nodes, id: \.self, selection: $selection) { node in + List(nodes, id: \.self, selection: $selection) { node in if nodes.count == 0 { Text("no.nodes").font(.title) } else { @@ -36,7 +38,7 @@ struct NodeList: View { let connected: Bool = (bleManager.connectedPeripheral != nil && bleManager.connectedPeripheral?.num ?? -1 == node.num) VStack(alignment: .leading) { HStack { - CircleText(text: node.user?.shortName ?? "???", color: .accentColor, circleSize: 52, fontSize: 16, brightness: 0.1) + CircleText(text: node.user?.shortName ?? "???", color: Color(node.num.uiColor()), circleSize: 52, fontSize: 16, brightness: 0.0, textColor: node.num.uiColor().isLight() ? .black : .white) .padding(.trailing, 5) VStack(alignment: .leading) { Text(node.user?.longName ?? NSLocalizedString("unknown", comment: "Unknown")).font(.headline) diff --git a/Meshtastic/Views/Settings/Config/Module/RangeTestConfig.swift b/Meshtastic/Views/Settings/Config/Module/RangeTestConfig.swift index c6fce6c5..45665dea 100644 --- a/Meshtastic/Views/Settings/Config/Module/RangeTestConfig.swift +++ b/Meshtastic/Views/Settings/Config/Module/RangeTestConfig.swift @@ -72,7 +72,7 @@ struct RangeTestConfig: View { .font(.caption) } } - .disabled(self.bleManager.connectedPeripheral == nil || node?.positionConfig == nil || !(node != nil && node?.metadata?.hasWifi ?? false)) + .disabled(self.bleManager.connectedPeripheral == nil || node?.rangeTestConfig == nil || !(node != nil && node?.metadata?.hasWifi ?? false)) Button { isPresentingSaveConfirm = true } label: {