From 02cca19f263a5e266240353eb6c14eeb5ffe7dff Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Sat, 23 Mar 2024 09:01:44 -0700 Subject: [PATCH 01/34] Assorted updates --- Meshtastic.xcodeproj/project.pbxproj | 14 +- Meshtastic/Enums/MessagingEnums.swift | 19 +- .../CoreData/PositionEntityExtension.swift | 11 + Meshtastic/Helpers/MeshPackets.swift | 18 -- Meshtastic/Persistence/UpdateCoreData.swift | 15 +- .../Protobufs/meshtastic/deviceonly.pb.swift | 16 ++ Meshtastic/Protobufs/meshtastic/mesh.pb.swift | 16 ++ .../Views/Messages/UserMessageList.swift | 2 +- .../Map/MapContent/MeshMapContent.swift | 162 ++++++++++++++ .../Map/{ => MapContent}/NodeMapContent.swift | 0 .../Views/Nodes/Helpers/NodeListItem.swift | 2 +- Meshtastic/Views/Nodes/MeshMap.swift | 200 ++++++------------ de.lproj/Localizable.strings | 1 + en.lproj/Localizable.strings | 1 + fr.lproj/Localizable.strings | 1 + he.lproj/Localizable.strings | 1 + pl.lproj/Localizable.strings | 1 + protobufs | 2 +- zh-Hans.lproj/Localizable.strings | 1 + zh-Hant-TW.lproj/Localizable.strings | 1 + 20 files changed, 318 insertions(+), 166 deletions(-) create mode 100644 Meshtastic/Views/Nodes/Helpers/Map/MapContent/MeshMapContent.swift rename Meshtastic/Views/Nodes/Helpers/Map/{ => MapContent}/NodeMapContent.swift (100%) diff --git a/Meshtastic.xcodeproj/project.pbxproj b/Meshtastic.xcodeproj/project.pbxproj index 87b0ed30..ebc7efee 100644 --- a/Meshtastic.xcodeproj/project.pbxproj +++ b/Meshtastic.xcodeproj/project.pbxproj @@ -185,6 +185,7 @@ DDDB445029F8AC9C00EE2349 /* UIImage.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDDB444F29F8AC9C00EE2349 /* UIImage.swift */; }; DDDB445229F8ACF900EE2349 /* Date.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDDB445129F8ACF900EE2349 /* Date.swift */; }; DDDB445429F8AD1600EE2349 /* Data.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDDB445329F8AD1600EE2349 /* Data.swift */; }; + DDDC22382BA92344002C44F1 /* MeshMapContent.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDDC22372BA92344002C44F1 /* MeshMapContent.swift */; }; DDDE59F529AF163D00490C6C /* WidgetKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = DD41A61C29AE7E8E003C5A37 /* WidgetKit.framework */; }; DDDE59F629AF163D00490C6C /* SwiftUI.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = DD41A61E29AE7E8F003C5A37 /* SwiftUI.framework */; }; DDDE59F929AF163D00490C6C /* WidgetsBundle.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDDE59F829AF163D00490C6C /* WidgetsBundle.swift */; }; @@ -456,6 +457,7 @@ DDDB445329F8AD1600EE2349 /* Data.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Data.swift; sourceTree = ""; }; DDDC22312BA76701002C44F1 /* fr */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = fr; path = fr.lproj/Localizable.strings; sourceTree = ""; }; DDDC22322BA76961002C44F1 /* zh-Hant-TW */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "zh-Hant-TW"; path = "zh-Hant-TW.lproj/Localizable.strings"; sourceTree = ""; }; + DDDC22372BA92344002C44F1 /* MeshMapContent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MeshMapContent.swift; sourceTree = ""; }; DDDD527729B5B83F0045BC3C /* MeshtasticDataModelV9.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = MeshtasticDataModelV9.xcdatamodel; sourceTree = ""; }; DDDE59F429AF163D00490C6C /* WidgetsExtension.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = WidgetsExtension.appex; sourceTree = BUILT_PRODUCTS_DIR; }; DDDE59F829AF163D00490C6C /* WidgetsBundle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WidgetsBundle.swift; sourceTree = ""; }; @@ -747,7 +749,7 @@ DDAD49EB2AFAE82500B4425D /* Map */ = { isa = PBXGroup; children = ( - DD93800A2BA3F968008BEC06 /* NodeMapContent.swift */, + DDDC22362BA9232C002C44F1 /* MapContent */, DD94B73F2ACCE3BE00DCD1D1 /* MapSettingsForm.swift */, DDB6CCFA2AAF805100945AF6 /* NodeMapSwiftUI.swift */, DD13AA482AB73BF400BA0C98 /* PositionPopover.swift */, @@ -986,6 +988,15 @@ path = Extensions; sourceTree = ""; }; + DDDC22362BA9232C002C44F1 /* MapContent */ = { + isa = PBXGroup; + children = ( + DDDC22372BA92344002C44F1 /* MeshMapContent.swift */, + DD93800A2BA3F968008BEC06 /* NodeMapContent.swift */, + ); + path = MapContent; + sourceTree = ""; + }; DDDE59F729AF163D00490C6C /* Widgets */ = { isa = PBXGroup; children = ( @@ -1214,6 +1225,7 @@ DD5E523F298F5A9E00D21B61 /* AirQualityIndexCompact.swift in Sources */, DD964FBF296E76EF007C176F /* WaypointFormMapKit.swift in Sources */, DD3501892852FC3B000FC853 /* Settings.swift in Sources */, + DDDC22382BA92344002C44F1 /* MeshMapContent.swift in Sources */, DDDB443629F6287000EE2349 /* MapButtons.swift in Sources */, DD5D0A9C2931B9F200F7EA61 /* EthernetModes.swift in Sources */, 6DEDA55A2A957B8E00321D2E /* DetectionSensorLog.swift in Sources */, diff --git a/Meshtastic/Enums/MessagingEnums.swift b/Meshtastic/Enums/MessagingEnums.swift index 010c324a..193060fa 100644 --- a/Meshtastic/Enums/MessagingEnums.swift +++ b/Meshtastic/Enums/MessagingEnums.swift @@ -13,17 +13,20 @@ enum BubblePosition { enum Tapbacks: Int, CaseIterable, Identifiable { - case heart = 0 - case thumbsUp = 1 - case thumbsDown = 2 - case haHa = 3 - case exclamation = 4 - case question = 5 - case poop = 6 + case wave = 0 + case heart = 1 + case thumbsUp = 2 + case thumbsDown = 3 + case haHa = 4 + case exclamation = 5 + case question = 6 + case poop = 7 var id: Int { self.rawValue } var emojiString: String { switch self { + case .wave: + return "👋" case .heart: return "❤️" case .thumbsUp: @@ -42,6 +45,8 @@ enum Tapbacks: Int, CaseIterable, Identifiable { } var description: String { switch self { + case .wave: + return "tapback.wave".localized case .heart: return "tapback.heart".localized case .thumbsUp: diff --git a/Meshtastic/Extensions/CoreData/PositionEntityExtension.swift b/Meshtastic/Extensions/CoreData/PositionEntityExtension.swift index d9079158..4b9ce0c2 100644 --- a/Meshtastic/Extensions/CoreData/PositionEntityExtension.swift +++ b/Meshtastic/Extensions/CoreData/PositionEntityExtension.swift @@ -11,6 +11,17 @@ import MapKit import SwiftUI extension PositionEntity { + + static func allPositionsFetchRequest() -> NSFetchRequest { + let request: NSFetchRequest = PositionEntity.fetchRequest() + // request.fetchLimit = 100 + //request.fetchBatchSize = 2 + //request.includesSubentities = false + request.returnsDistinctResults = true + request.sortDescriptors = [NSSortDescriptor(key: "time", ascending: true)] + request.predicate = NSPredicate(format: "nodePosition != nil && latest == true", Calendar.current.date(byAdding: .day, value: -7, to: Date())! as NSDate) + return request + } var latitude: Double? { diff --git a/Meshtastic/Helpers/MeshPackets.swift b/Meshtastic/Helpers/MeshPackets.swift index c02835e7..e89aeb82 100644 --- a/Meshtastic/Helpers/MeshPackets.swift +++ b/Meshtastic/Helpers/MeshPackets.swift @@ -724,24 +724,6 @@ func telemetryPacket(packet: MeshPacket, connectedNode: Int64, context: NSManage ) ] manager.schedule() - -// let content = UNMutableNotificationContent() -// content.title = "Critically Low Battery!" -// content.body = "Time to charge your radio, there is \(telemetry.batteryLevel)% battery remaining." -// content.userInfo["target"] = "node" -// content.userInfo["path"] = "meshtastic://node/\(telemetry.nodeTelemetry?.num ?? 0)" -// let trigger = UNTimeIntervalNotificationTrigger(timeInterval: 1, repeats: false) -// let uuidString = UUID().uuidString -// let request = UNNotificationRequest(identifier: uuidString, content: content, trigger: trigger) -// let notificationCenter = UNUserNotificationCenter.current() -// notificationCenter.add(request) { (error) in -// if error != nil { -// // Handle any errors. -// print("Error creating local low battery notification: \(error?.localizedDescription ?? "no description")") -// } else { -// print("Created local low battery notification.") -// } -// } } // Update our live activity if there is one running, not available on mac iOS >= 16.2 #if !targetEnvironment(macCatalyst) diff --git a/Meshtastic/Persistence/UpdateCoreData.swift b/Meshtastic/Persistence/UpdateCoreData.swift index 121cab33..98561c0d 100644 --- a/Meshtastic/Persistence/UpdateCoreData.swift +++ b/Meshtastic/Persistence/UpdateCoreData.swift @@ -148,7 +148,9 @@ func upsertNodeInfoPacket (packet: MeshPacket, context: NSManagedObjectContext) newNode.channel = Int32(packet.channel) } if let nodeInfoMessage = try? NodeInfo(serializedData: packet.decoded.payload) { - newNode.hopsAway = Int32(truncatingIfNeeded: nodeInfoMessage.hopsAway) + newNode.hopsAway = Int32(nodeInfoMessage.hopsAway) + } else if packet.hopStart != 0 && packet.hopLimit <= packet.hopStart { + newNode.hopsAway = Int32(packet.hopStart - packet.hopLimit) } if let newUserMessage = try? User(serializedData: packet.decoded.payload) { @@ -218,8 +220,8 @@ func upsertNodeInfoPacket (packet: MeshPacket, context: NSManagedObjectContext) } if let nodeInfoMessage = try? NodeInfo(serializedData: packet.decoded.payload) { - fetchedNode[0].channel = Int32(nodeInfoMessage.channel) - fetchedNode[0].hopsAway = Int32(truncatingIfNeeded: nodeInfoMessage.hopsAway) + + fetchedNode[0].hopsAway = Int32(nodeInfoMessage.hopsAway) if nodeInfoMessage.hasDeviceMetrics { let telemetry = TelemetryEntity(context: context) telemetry.batteryLevel = Int32(nodeInfoMessage.deviceMetrics.batteryLevel) @@ -231,6 +233,7 @@ func upsertNodeInfoPacket (packet: MeshPacket, context: NSManagedObjectContext) fetchedNode[0].telemetries? = NSOrderedSet(array: newTelemetries) } if nodeInfoMessage.hasUser { + fetchedNode[0].user!.vip = nodeInfoMessage.isFavorite /// Seeing Some crashes here ? fetchedNode[0].user!.userId = nodeInfoMessage.user.id fetchedNode[0].user!.num = Int64(nodeInfoMessage.num) @@ -239,6 +242,8 @@ func upsertNodeInfoPacket (packet: MeshPacket, context: NSManagedObjectContext) fetchedNode[0].user!.role = Int32(nodeInfoMessage.user.role.rawValue) fetchedNode[0].user!.hwModel = String(describing: nodeInfoMessage.user.hwModel).uppercased() } + } else if packet.hopStart != 0 && packet.hopLimit <= packet.hopStart { + fetchedNode[0].hopsAway = Int32(packet.hopStart - packet.hopLimit) } if (fetchedNode[0].user == nil) { let newUser = UserEntity(context: context) @@ -317,9 +322,9 @@ func upsertPositionPacket (packet: MeshPacket, context: NSManagedObjectContext) return } /// Don't save nearly the same position over and over. If the next position is less than 10 meters from the new position, delete the previous position and save the new one. - if mutablePositions.count > 0 && position.precisionBits == 32 { + if mutablePositions.count > 0 && (position.precisionBits == 32 || position.precisionBits == 0) { let mostRecent = mutablePositions.lastObject as! PositionEntity - if mostRecent.coordinate.distance(from: position.coordinate) < 15 { + if mostRecent.coordinate.distance(from: position.coordinate) < 15.0 { mutablePositions.remove(mostRecent) } } else if mutablePositions.count > 0 && 11...16 ~= position.precisionBits { diff --git a/Meshtastic/Protobufs/meshtastic/deviceonly.pb.swift b/Meshtastic/Protobufs/meshtastic/deviceonly.pb.swift index 950c577e..f25a5cd1 100644 --- a/Meshtastic/Protobufs/meshtastic/deviceonly.pb.swift +++ b/Meshtastic/Protobufs/meshtastic/deviceonly.pb.swift @@ -269,6 +269,14 @@ struct NodeInfoLite { set {_uniqueStorage()._hopsAway = newValue} } + /// + /// True if node is in our favorites list + /// Persists between NodeDB internal clean ups + var isFavorite: Bool { + get {return _storage._isFavorite} + set {_uniqueStorage()._isFavorite = newValue} + } + var unknownFields = SwiftProtobuf.UnknownStorage() init() {} @@ -600,6 +608,7 @@ extension NodeInfoLite: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementat 7: .same(proto: "channel"), 8: .standard(proto: "via_mqtt"), 9: .standard(proto: "hops_away"), + 10: .standard(proto: "is_favorite"), ] fileprivate class _StorageClass { @@ -612,6 +621,7 @@ extension NodeInfoLite: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementat var _channel: UInt32 = 0 var _viaMqtt: Bool = false var _hopsAway: UInt32 = 0 + var _isFavorite: Bool = false static let defaultInstance = _StorageClass() @@ -627,6 +637,7 @@ extension NodeInfoLite: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementat _channel = source._channel _viaMqtt = source._viaMqtt _hopsAway = source._hopsAway + _isFavorite = source._isFavorite } } @@ -654,6 +665,7 @@ extension NodeInfoLite: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementat case 7: try { try decoder.decodeSingularUInt32Field(value: &_storage._channel) }() case 8: try { try decoder.decodeSingularBoolField(value: &_storage._viaMqtt) }() case 9: try { try decoder.decodeSingularUInt32Field(value: &_storage._hopsAway) }() + case 10: try { try decoder.decodeSingularBoolField(value: &_storage._isFavorite) }() default: break } } @@ -693,6 +705,9 @@ extension NodeInfoLite: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementat if _storage._hopsAway != 0 { try visitor.visitSingularUInt32Field(value: _storage._hopsAway, fieldNumber: 9) } + if _storage._isFavorite != false { + try visitor.visitSingularBoolField(value: _storage._isFavorite, fieldNumber: 10) + } } try unknownFields.traverse(visitor: &visitor) } @@ -711,6 +726,7 @@ extension NodeInfoLite: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementat if _storage._channel != rhs_storage._channel {return false} if _storage._viaMqtt != rhs_storage._viaMqtt {return false} if _storage._hopsAway != rhs_storage._hopsAway {return false} + if _storage._isFavorite != rhs_storage._isFavorite {return false} return true } if !storagesAreEqual {return false} diff --git a/Meshtastic/Protobufs/meshtastic/mesh.pb.swift b/Meshtastic/Protobufs/meshtastic/mesh.pb.swift index 2890db6c..18da455b 100644 --- a/Meshtastic/Protobufs/meshtastic/mesh.pb.swift +++ b/Meshtastic/Protobufs/meshtastic/mesh.pb.swift @@ -1861,6 +1861,14 @@ struct NodeInfo { set {_uniqueStorage()._hopsAway = newValue} } + /// + /// True if node is in our favorites list + /// Persists between NodeDB internal clean ups + var isFavorite: Bool { + get {return _storage._isFavorite} + set {_uniqueStorage()._isFavorite = newValue} + } + var unknownFields = SwiftProtobuf.UnknownStorage() init() {} @@ -3647,6 +3655,7 @@ extension NodeInfo: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationB 7: .same(proto: "channel"), 8: .standard(proto: "via_mqtt"), 9: .standard(proto: "hops_away"), + 10: .standard(proto: "is_favorite"), ] fileprivate class _StorageClass { @@ -3659,6 +3668,7 @@ extension NodeInfo: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationB var _channel: UInt32 = 0 var _viaMqtt: Bool = false var _hopsAway: UInt32 = 0 + var _isFavorite: Bool = false static let defaultInstance = _StorageClass() @@ -3674,6 +3684,7 @@ extension NodeInfo: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationB _channel = source._channel _viaMqtt = source._viaMqtt _hopsAway = source._hopsAway + _isFavorite = source._isFavorite } } @@ -3701,6 +3712,7 @@ extension NodeInfo: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationB case 7: try { try decoder.decodeSingularUInt32Field(value: &_storage._channel) }() case 8: try { try decoder.decodeSingularBoolField(value: &_storage._viaMqtt) }() case 9: try { try decoder.decodeSingularUInt32Field(value: &_storage._hopsAway) }() + case 10: try { try decoder.decodeSingularBoolField(value: &_storage._isFavorite) }() default: break } } @@ -3740,6 +3752,9 @@ extension NodeInfo: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationB if _storage._hopsAway != 0 { try visitor.visitSingularUInt32Field(value: _storage._hopsAway, fieldNumber: 9) } + if _storage._isFavorite != false { + try visitor.visitSingularBoolField(value: _storage._isFavorite, fieldNumber: 10) + } } try unknownFields.traverse(visitor: &visitor) } @@ -3758,6 +3773,7 @@ extension NodeInfo: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationB if _storage._channel != rhs_storage._channel {return false} if _storage._viaMqtt != rhs_storage._viaMqtt {return false} if _storage._hopsAway != rhs_storage._hopsAway {return false} + if _storage._isFavorite != rhs_storage._isFavorite {return false} return true } if !storagesAreEqual {return false} diff --git a/Meshtastic/Views/Messages/UserMessageList.swift b/Meshtastic/Views/Messages/UserMessageList.swift index 7f0660ee..c62def59 100644 --- a/Meshtastic/Views/Messages/UserMessageList.swift +++ b/Meshtastic/Views/Messages/UserMessageList.swift @@ -73,7 +73,7 @@ struct UserMessageList: View { if message.realACK { Text("\(ackErrorVal?.display ?? "Empty Ack Error")").font(.caption2).foregroundColor(.gray) } else { - Text("Implicit ACK from another node").font(.caption2).foregroundColor(.orange) + Text("Acknowledged by another node").font(.caption2).foregroundColor(.orange) } } else if currentUser && message.ackError == 0 { // Empty Error diff --git a/Meshtastic/Views/Nodes/Helpers/Map/MapContent/MeshMapContent.swift b/Meshtastic/Views/Nodes/Helpers/Map/MapContent/MeshMapContent.swift new file mode 100644 index 00000000..0e0c519f --- /dev/null +++ b/Meshtastic/Views/Nodes/Helpers/Map/MapContent/MeshMapContent.swift @@ -0,0 +1,162 @@ +// +// MeshMapContent.swift +// Meshtastic +// +// Created by Garth Vander Houwen on 3/17/24. +// + +import SwiftUI +import MapKit + +import SwiftUI +import MapKit + +@available(iOS 17.0, macOS 14.0, *) +struct MeshMapContent: MapContent { + + @State var positions: [PositionEntity] = [] + @State var waypoints: [WaypointEntity] = [] + @State var routes: [RouteEntity] = [] + /// Parameters + @Binding var showUserLocation: Bool + @Binding var showNodeHistory: Bool + @Binding var showRouteLines: Bool + @Binding var showConvexHull: Bool + @Binding var showTraffic: Bool + @Binding var showPointsOfInterest: Bool + @Binding var selectedMapLayer: MapLayer + // Map Configuration + ///@Namespace var mapScope + + @State var mapStyle: MapStyle = MapStyle.standard(elevation: .realistic, emphasis: MapStyle.StandardEmphasis.muted ,pointsOfInterest: .excludingAll, showsTraffic: false) + //@State var position = MapCameraPosition.automatic + //@State var scene: MKLookAroundScene? + //@State var isLookingAround = false + //@State var isEditingSettings = false + @Binding var selectedPosition: PositionEntity? + @Binding var showWaypoints: Bool + //@Binding var editingWaypoint: WaypointEntity? + @Binding var selectedWaypoint: WaypointEntity? + + var delay: Double = 0 + @State private var scale: CGFloat = 0.5 + + @MapContentBuilder + var meshMap: some MapContent { + let lineCoords = positions.compactMap({(position) -> CLLocationCoordinate2D in + return position.nodeCoordinate ?? LocationsHandler.DefaultLocation + }) + /// Convex Hull + if showConvexHull { + if lineCoords.count > 0 { + let hull = lineCoords.getConvexHull() + MapPolygon(coordinates: hull) + .stroke(.blue, lineWidth: 3) + .foregroundStyle(.indigo.opacity(0.4)) + } + } + /// Position Annotations + ForEach(Array(positions), id: \.id) { position in + /// Node color from node.num + let nodeColor = UIColor(hex: UInt32(position.nodePosition?.num ?? 0)) + Annotation(position.nodePosition?.user?.longName ?? "?", coordinate: position.coordinate) { + LazyVStack { + ZStack { + let nodeColor = UIColor(hex: UInt32(position.nodePosition?.num ?? 0)) + if position.nodePosition?.isOnline ?? false { + Circle() + .fill(Color(nodeColor.lighter()).opacity(0.4).shadow(.drop(color: Color(nodeColor).isLight() ? .black : .white, radius: 5))) + .foregroundStyle(Color(nodeColor.lighter()).opacity(0.3)) +// .scaleEffect(scale) +// .animation( +// Animation.easeInOut(duration: 0.6) +// .repeatForever().delay(delay), value: scale +// ) +// .onAppear { +// self.scale = 1 +// } +// .frame(width: 60, height: 60) + } + if position.nodePosition?.hasDetectionSensorMetrics ?? false { + Image(systemName: "sensor.fill") + .symbolRenderingMode(.palette) + .symbolEffect(.variableColor) + .padding() + .foregroundStyle(.white) + .background(Color(nodeColor)) + .clipShape(Circle()) + } else { + CircleText(text: position.nodePosition?.user?.shortName ?? "?", color: Color(nodeColor), circleSize: 40) + } + } + } + .onTapGesture { location in + selectedPosition = (selectedPosition == position ? nil : position) + } + } + /// Reduced Precision Map Circles + if 11...16 ~= position.precisionBits { + let pp = PositionPrecision(rawValue: Int(position.precisionBits)) + let radius : CLLocationDistance = pp?.precisionMeters ?? 0 + if radius > 0.0 { + MapCircle(center: position.coordinate, radius: radius) + .foregroundStyle(Color(nodeColor).opacity(0.25)) + .stroke(.white, lineWidth: 2) + } + } + /// Routes + ForEach(Array(routes), id: \.id) { route in + let routeLocations = Array(route.locations!) as! [LocationEntity] + let routeCoords = routeLocations.compactMap({(loc) -> CLLocationCoordinate2D in + return loc.locationCoordinate ?? LocationHelper.DefaultLocation + }) + Annotation("Start", coordinate: routeCoords.first ?? LocationHelper.DefaultLocation) { + ZStack { + Circle() + .fill(Color(.green)) + .strokeBorder(.white, lineWidth: 3) + .frame(width: 15, height: 15) + } + } + .annotationTitles(.automatic) + Annotation("Finish", coordinate: routeCoords.last ?? LocationHelper.DefaultLocation) { + ZStack { + Circle() + .fill(Color(.black)) + .strokeBorder(.white, lineWidth: 3) + .frame(width: 15, height: 15) + } + } + .annotationTitles(.automatic) + let solid = StrokeStyle( + lineWidth: 3, + lineCap: .round, lineJoin: .round + ) + MapPolyline(coordinates: routeCoords) + .stroke(Color(UIColor(hex: UInt32(route.color))), style: solid) + + } + } + + /// Waypoint Annotations + if waypoints.count > 0 && showWaypoints { + ForEach(Array(waypoints), id: \.id) { waypoint in + Annotation(waypoint.name ?? "?", coordinate: waypoint.coordinate) { + LazyVStack { + CircleText(text: String(UnicodeScalar(Int(waypoint.icon)) ?? "📍"), color: Color.orange, circleSize: 40) + .onTapGesture(perform: { location in + selectedWaypoint = (selectedWaypoint == waypoint ? nil : waypoint) + }) + } + } + } + } + } + + @MapContentBuilder + var body: some MapContent { + if positions.count > 0 { + meshMap + } + } +} diff --git a/Meshtastic/Views/Nodes/Helpers/Map/NodeMapContent.swift b/Meshtastic/Views/Nodes/Helpers/Map/MapContent/NodeMapContent.swift similarity index 100% rename from Meshtastic/Views/Nodes/Helpers/Map/NodeMapContent.swift rename to Meshtastic/Views/Nodes/Helpers/Map/MapContent/NodeMapContent.swift diff --git a/Meshtastic/Views/Nodes/Helpers/NodeListItem.swift b/Meshtastic/Views/Nodes/Helpers/NodeListItem.swift index c880d27b..feb528f6 100644 --- a/Meshtastic/Views/Nodes/Helpers/NodeListItem.swift +++ b/Meshtastic/Views/Nodes/Helpers/NodeListItem.swift @@ -130,7 +130,7 @@ struct NodeListItem: View { .font(UIDevice.current.userInterfaceIdiom == .phone ? .callout : .caption) } } - + if node.viaMqtt && connectedNode != node.num { Image(systemName: "network") .symbolRenderingMode(.hierarchical) diff --git a/Meshtastic/Views/Nodes/MeshMap.swift b/Meshtastic/Views/Nodes/MeshMap.swift index 4394efce..ddc66314 100644 --- a/Meshtastic/Views/Nodes/MeshMap.swift +++ b/Meshtastic/Views/Nodes/MeshMap.swift @@ -13,6 +13,8 @@ import Foundation import MapKit #endif + + @available(iOS 17.0, macOS 14.0, *) struct MeshMap: View { @@ -32,22 +34,36 @@ struct MeshMap: View { @Namespace var mapScope @State var mapStyle: MapStyle = MapStyle.standard(elevation: .realistic, emphasis: MapStyle.StandardEmphasis.muted ,pointsOfInterest: .all, showsTraffic: true) @State var position = MapCameraPosition.automatic - @State var scene: MKLookAroundScene? - @State var isLookingAround = false + //@State var scene: MKLookAroundScene? + //@State var isLookingAround = false @State var isEditingSettings = false @State var selectedPosition: PositionEntity? @State var showWaypoints = true @State var editingWaypoint: WaypointEntity? @State var selectedWaypoint: WaypointEntity? - @State var newWaypointCoord :CLLocationCoordinate2D? + @State var newWaypointCoord: CLLocationCoordinate2D? @State var isMeshMap = true - var delay: Double = 0 - @State private var scale: CGFloat = 0.5 + let positionRequest: NSFetchRequest = { + // Create a fetch request. + let request = PositionEntity.fetchRequest() + + // Limit the maximum number of items that the request returns. + request.fetchLimit = 100 + + // Filter the request results, such as to only return unchecked items. + request.predicate = NSPredicate(format: "nodePosition != nil && latest == true && time >= %@", Calendar.current.date(byAdding: .hour, value: -6, to: Date())! as NSDate) + + // Sort the fetched results + request.sortDescriptors = [NSSortDescriptor(key: "time", ascending: true)] + + + return request + }() + /// && time >= %@ - @FetchRequest(sortDescriptors: [NSSortDescriptor(key: "time", ascending: true)], - predicate: NSPredicate(format: "nodePosition != nil && latest == true", Calendar.current.date(byAdding: .day, value: -7, to: Date())! as NSDate), animation: .none) - private var positions: FetchedResults + @FetchRequest(fetchRequest: PositionEntity.allPositionsFetchRequest(), animation: .none) + var positions: FetchedResults @FetchRequest(sortDescriptors: [NSSortDescriptor(key: "name", ascending: false)], predicate: NSPredicate( @@ -61,118 +77,46 @@ struct MeshMap: View { var body: some View { - let lineCoords = Array(positions).compactMap({(position) -> CLLocationCoordinate2D in - return position.nodeCoordinate ?? LocationHelper.DefaultLocation - }) NavigationStack { ZStack { MapReader { reader in Map(position: $position, bounds: MapCameraBounds(minimumDistance: 1, maximumDistance: .infinity), scope: mapScope) { - /// Convex Hull - if showConvexHull { - if lineCoords.count > 0 { - let hull = lineCoords.getConvexHull() - MapPolygon(coordinates: hull) - .stroke(.blue, lineWidth: 3) - .foregroundStyle(.indigo.opacity(0.4)) - } - } - /// Position Annotations - ForEach(Array(positions), id: \.id) { position in - /// Node color from node.num - let nodeColor = UIColor(hex: UInt32(position.nodePosition?.num ?? 0)) - Annotation(position.nodePosition?.user?.longName ?? "?", coordinate: position.coordinate) { - LazyVStack { - ZStack { - let nodeColor = UIColor(hex: UInt32(position.nodePosition?.num ?? 0)) - if position.nodePosition?.isOnline ?? false { - Circle() - .fill(Color(nodeColor.lighter()).opacity(0.4).shadow(.drop(color: Color(nodeColor).isLight() ? .black : .white, radius: 5))) - .foregroundStyle(Color(nodeColor.lighter()).opacity(0.3)) - .scaleEffect(scale) - .animation( - Animation.easeInOut(duration: 0.6) - .repeatForever().delay(delay), value: scale - ) - .onAppear { - self.scale = 1 - } - .frame(width: 60, height: 60) - } - if position.nodePosition?.hasDetectionSensorMetrics ?? false { - Image(systemName: "sensor.fill") - .symbolRenderingMode(.palette) - .symbolEffect(.variableColor) - .padding() - .foregroundStyle(.white) - .background(Color(nodeColor)) - .clipShape(Circle()) - } else { - CircleText(text: position.nodePosition?.user?.shortName ?? "?", color: Color(nodeColor), circleSize: 40) - } - } - } - .onTapGesture { location in - selectedPosition = (selectedPosition == position ? nil : position) - } - } - /// Reduced Precision Map Circles - if 11...16 ~= position.precisionBits { - let pp = PositionPrecision(rawValue: Int(position.precisionBits)) - let radius : CLLocationDistance = pp?.precisionMeters ?? 0 - if radius > 0.0 { - MapCircle(center: position.coordinate, radius: radius) - .foregroundStyle(Color(nodeColor).opacity(0.25)) - .stroke(.white, lineWidth: 2) - } - } - /// Routes - ForEach(Array(routes), id: \.id) { route in - let routeLocations = Array(route.locations!) as! [LocationEntity] - let routeCoords = routeLocations.compactMap({(loc) -> CLLocationCoordinate2D in - return loc.locationCoordinate ?? LocationHelper.DefaultLocation - }) - Annotation("Start", coordinate: routeCoords.first ?? LocationHelper.DefaultLocation) { - ZStack { - Circle() - .fill(Color(.green)) - .strokeBorder(.white, lineWidth: 3) - .frame(width: 15, height: 15) - } - } - .annotationTitles(.automatic) - Annotation("Finish", coordinate: routeCoords.last ?? LocationHelper.DefaultLocation) { - ZStack { - Circle() - .fill(Color(.black)) - .strokeBorder(.white, lineWidth: 3) - .frame(width: 15, height: 15) - } - } - .annotationTitles(.automatic) - let solid = StrokeStyle( - lineWidth: 3, - lineCap: .round, lineJoin: .round - ) - MapPolyline(coordinates: routeCoords) - .stroke(Color(UIColor(hex: UInt32(route.color))), style: solid) - - } - } - - /// Waypoint Annotations - if waypoints.count > 0 && showWaypoints { - ForEach(Array(waypoints), id: \.id) { waypoint in - Annotation(waypoint.name ?? "?", coordinate: waypoint.coordinate) { - LazyVStack { - CircleText(text: String(UnicodeScalar(Int(waypoint.icon)) ?? "📍"), color: Color.orange, circleSize: 40) - .onTapGesture(perform: { location in - selectedWaypoint = (selectedWaypoint == waypoint ? nil : waypoint) - }) - } - } - } - } + MeshMapContent(positions: Array(positions), waypoints: Array(waypoints), routes: Array(routes), showUserLocation: $showUserLocation, showNodeHistory: $showNodeHistory, showRouteLines: $showRouteLines, showConvexHull: $showConvexHull, showTraffic: $showTraffic, showPointsOfInterest: $showPointsOfInterest, selectedMapLayer: $selectedMapLayer, selectedPosition: $selectedPosition, showWaypoints: $showWaypoints, selectedWaypoint: $selectedWaypoint) + +// /// Routes +// ForEach(Array(routes), id: \.id) { route in +// let routeLocations = Array(route.locations!) as! [LocationEntity] +// let routeCoords = routeLocations.compactMap({(loc) -> CLLocationCoordinate2D in +// return loc.locationCoordinate ?? LocationHelper.DefaultLocation +// }) +// Annotation("Start", coordinate: routeCoords.first ?? LocationHelper.DefaultLocation) { +// ZStack { +// Circle() +// .fill(Color(.green)) +// .strokeBorder(.white, lineWidth: 3) +// .frame(width: 15, height: 15) +// } +// } +// .annotationTitles(.automatic) +// Annotation("Finish", coordinate: routeCoords.last ?? LocationHelper.DefaultLocation) { +// ZStack { +// Circle() +// .fill(Color(.black)) +// .strokeBorder(.white, lineWidth: 3) +// .frame(width: 15, height: 15) +// } +// } +// .annotationTitles(.automatic) +// let solid = StrokeStyle( +// lineWidth: 3, +// lineCap: .round, lineJoin: .round +// ) +// MapPolyline(coordinates: routeCoords) +// .stroke(Color(UIColor(hex: UInt32(route.color))), style: solid) +// +// } +// } +// } .mapScope(mapScope) .mapStyle(mapStyle) @@ -185,8 +129,14 @@ struct MeshMap: View { .mapControlVisibility(.automatic) } .controlSize(.regular) + .onTapGesture(count: 1, perform: { + position in + print(position) + // tapText = "map tap" + newWaypointCoord = reader.convert(position, from: .local) ?? CLLocationCoordinate2D.init() + }) .onTapGesture(count: 1, perform: { location in - newWaypointCoord = reader.convert(location , from: .local) + // newWaypointCoord = reader.convert(location , from: .local) }) .onLongPressGesture(minimumDuration: 0.5, maximumDistance: 10) { editingWaypoint = WaypointEntity(context: context) @@ -286,26 +236,12 @@ struct MeshMap: View { .foregroundColor(.accentColor) .buttonStyle(.borderedProminent) } - /// Look Around Button - if self.scene != nil { - Button(action: { - withAnimation { - isLookingAround = !isLookingAround - } - }) { - Image(systemName: isLookingAround ? "binoculars.fill" : "binoculars") - .padding(.vertical, 5) - } - .tint(Color(UIColor.secondarySystemBackground)) - .foregroundColor(.accentColor) - .buttonStyle(.borderedProminent) - } } .controlSize(.regular) .padding(5) } } - .navigationTitle("Mesh Map") + .navigationTitle("\(positions.count) Nodes") .navigationBarItems(leading: MeshtasticLogo(), trailing: ZStack { ConnectedDevice(bluetoothOn: bleManager.isSwitchedOn, deviceConnected: bleManager.connectedPeripheral != nil, name: (bleManager.connectedPeripheral != nil) ? bleManager.connectedPeripheral.shortName : "?") }) diff --git a/de.lproj/Localizable.strings b/de.lproj/Localizable.strings index ada6bdc1..2599d7b2 100644 --- a/de.lproj/Localizable.strings +++ b/de.lproj/Localizable.strings @@ -309,6 +309,7 @@ "tapback.exclamation"="Ausrufezeichen"; "tapback.question"="Fragezeichen"; "tapback.poop"="Kacke"; +"tapback.wave"="Wave"; "telemetry"="Telemetrie (Sensoren)"; "telemetry.config"="Telemetrie Einstellungen"; "timeout"="Zeitlimit erreicht"; diff --git a/en.lproj/Localizable.strings b/en.lproj/Localizable.strings index 69961f21..251e1b0d 100644 --- a/en.lproj/Localizable.strings +++ b/en.lproj/Localizable.strings @@ -326,6 +326,7 @@ "tapback.exclamation"="Exclamation Mark"; "tapback.question"="Question Mark"; "tapback.poop"="Poop"; +"tapback.wave"="Wave"; "telemetry"="Telemetry (Sensors)"; "telemetry.config"="Telemetry Config"; "timeout"="Timeout"; diff --git a/fr.lproj/Localizable.strings b/fr.lproj/Localizable.strings index 4a5ea20d..31642beb 100644 --- a/fr.lproj/Localizable.strings +++ b/fr.lproj/Localizable.strings @@ -292,6 +292,7 @@ "tapback.exclamation"="Point d'exclamation"; "tapback.question"="Point d'interrogation"; "tapback.poop"="Caca"; +"tapback.wave"="Wave"; "telemetry"="Télémetrie (Capteurs)"; "telemetry.config"="Configuration de télémetrie"; "timeout"="Délai d'expiration"; diff --git a/he.lproj/Localizable.strings b/he.lproj/Localizable.strings index c4418a90..22e5a2df 100644 --- a/he.lproj/Localizable.strings +++ b/he.lproj/Localizable.strings @@ -316,6 +316,7 @@ "tapback.exclamation"="סימן קריאה"; "tapback.question"="סימן שאלה"; "tapback.poop"="חרא"; +"tapback.wave"="Wave"; "telemetry"="טלמטריה (חיישנים)"; "telemetry.config"="הגדרות טלמטריה"; "timeout"="זמן קצוב"; diff --git a/pl.lproj/Localizable.strings b/pl.lproj/Localizable.strings index b50eff01..2c5aa853 100644 --- a/pl.lproj/Localizable.strings +++ b/pl.lproj/Localizable.strings @@ -310,6 +310,7 @@ "tapback.exclamation"="Wykrzyknik"; "tapback.question"="Znak zapytania"; "tapback.poop"="Kupa"; +"tapback.wave"="Wave"; "telemetry"="Telemetria (czujniki)"; "telemetry.config"="Konfiguracja telemetrii"; "timeout"="Limit czasu"; diff --git a/protobufs b/protobufs index 556e49ba..bcfb49c4 160000 --- a/protobufs +++ b/protobufs @@ -1 +1 @@ -Subproject commit 556e49ba619e2f4d8fa3c2dee2a94129a43d5f08 +Subproject commit bcfb49c4988b1539fc35e568a58b9f2f5b60738a diff --git a/zh-Hans.lproj/Localizable.strings b/zh-Hans.lproj/Localizable.strings index 67c9ac65..6c8f6b0a 100644 --- a/zh-Hans.lproj/Localizable.strings +++ b/zh-Hans.lproj/Localizable.strings @@ -309,6 +309,7 @@ "tapback.exclamation"="感叹号"; "tapback.question"="问号"; "tapback.poop"="便便"; +"tapback.wave"="Wave"; "telemetry"="遥测(传感器)"; "telemetry.config"="遥测配置"; "timeout"="超时"; diff --git a/zh-Hant-TW.lproj/Localizable.strings b/zh-Hant-TW.lproj/Localizable.strings index 6f36c512..d0f8e876 100644 --- a/zh-Hant-TW.lproj/Localizable.strings +++ b/zh-Hant-TW.lproj/Localizable.strings @@ -309,6 +309,7 @@ "tapback.exclamation"="驚嘆號"; "tapback.question"="問號"; "tapback.poop"="便便"; +"tapback.wave"="Wave"; "telemetry"="遠測(傳感器)"; "telemetry.config"="遠側設定"; "timeout"="超時"; From 6d073d008629a5e29c973b78d75a1d3011e62738 Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Sat, 23 Mar 2024 09:11:18 -0700 Subject: [PATCH 02/34] Bump version --- Meshtastic.xcodeproj/project.pbxproj | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Meshtastic.xcodeproj/project.pbxproj b/Meshtastic.xcodeproj/project.pbxproj index ebc7efee..67e51a8e 100644 --- a/Meshtastic.xcodeproj/project.pbxproj +++ b/Meshtastic.xcodeproj/project.pbxproj @@ -1603,7 +1603,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 2.3.1; + MARKETING_VERSION = 2.3.2; PRODUCT_BUNDLE_IDENTIFIER = gvh.MeshtasticClient; PRODUCT_NAME = "$(TARGET_NAME)"; SUPPORTS_MACCATALYST = YES; @@ -1637,7 +1637,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 2.3.1; + MARKETING_VERSION = 2.3.2; PRODUCT_BUNDLE_IDENTIFIER = gvh.MeshtasticClient; PRODUCT_NAME = "$(TARGET_NAME)"; SUPPORTS_MACCATALYST = YES; @@ -1759,7 +1759,7 @@ "@executable_path/Frameworks", "@executable_path/../../Frameworks", ); - MARKETING_VERSION = 2.3.1; + MARKETING_VERSION = 2.3.2; PRODUCT_BUNDLE_IDENTIFIER = gvh.MeshtasticClient.Widgets; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; @@ -1792,7 +1792,7 @@ "@executable_path/Frameworks", "@executable_path/../../Frameworks", ); - MARKETING_VERSION = 2.3.1; + MARKETING_VERSION = 2.3.2; PRODUCT_BUNDLE_IDENTIFIER = gvh.MeshtasticClient.Widgets; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; From e07c20a1ad5eb5463be1b5c97ee023d62617878e Mon Sep 17 00:00:00 2001 From: ChDel Date: Sat, 23 Mar 2024 14:18:41 -0700 Subject: [PATCH 03/34] Refactoring User Defaults extension --- Meshtastic/Extensions/UserDefaults.swift | 281 ++++++++--------------- 1 file changed, 95 insertions(+), 186 deletions(-) diff --git a/Meshtastic/Extensions/UserDefaults.swift b/Meshtastic/Extensions/UserDefaults.swift index 39cc9b89..c42ee9bc 100644 --- a/Meshtastic/Extensions/UserDefaults.swift +++ b/Meshtastic/Extensions/UserDefaults.swift @@ -7,6 +7,26 @@ import Foundation +@propertyWrapper +struct UserDefault { + let key: UserDefaults.Keys + let defaultValue: T + + init(_ key: UserDefaults.Keys, defaultValue: T) { + self.key = key + self.defaultValue = defaultValue + } + + var wrappedValue: T { + get { + UserDefaults.standard.object(forKey: key.rawValue) as? T ?? defaultValue + } + set { + UserDefaults.standard.set(newValue, forKey: key.rawValue) + } + } +} + extension UserDefaults { enum Keys: String, CaseIterable { case preferredPeripheralId @@ -18,10 +38,16 @@ extension UserDefaults { case meshMapShowNodeHistory case meshMapShowRouteLines case enableMapConvexHull + case enableMapRecentering + case enableMapNodeHistoryPins + case enableMapRouteLines case enableMapTraffic case enableMapPointsOfInterest case enableOfflineMaps + case enableOfflineMapsMBTiles case mapTileServer + case enableOverlayServer + case mapOverlayServer case mapTilesAboveLabels case mapUseLegacy case enableDetectionNotifications @@ -34,190 +60,73 @@ extension UserDefaults { func reset() { Keys.allCases.forEach { removeObject(forKey: $0.rawValue) } } - static var preferredPeripheralId: String { - get { - UserDefaults.standard.string(forKey: "preferredPeripheralId") ?? "" - } - set { - UserDefaults.standard.set(newValue, forKey: "preferredPeripheralId") - } - } - static var preferredPeripheralNum: Int { - get { - UserDefaults.standard.integer(forKey: "preferredPeripheralNum") - } - set { - UserDefaults.standard.set(newValue, forKey: "preferredPeripheralNum") - } - } - static var provideLocation: Bool { - get { - UserDefaults.standard.bool(forKey: "provideLocation") - } set { - UserDefaults.standard.set(newValue, forKey: "provideLocation") - } - } - static var provideLocationInterval: Int { - get { - UserDefaults.standard.integer(forKey: "provideLocationInterval") - } - set { - UserDefaults.standard.set(newValue, forKey: "provideLocationInterval") - } - } - static var mapLayer: MapLayer { - get { - MapLayer(rawValue: UserDefaults.standard.string(forKey: "mapLayer") ?? MapLayer.standard.rawValue) ?? MapLayer.standard - } - set { - UserDefaults.standard.set(newValue.rawValue, forKey: "mapLayer") - } - } - static var enableMapRecentering: Bool { - get { - UserDefaults.standard.bool(forKey: "meshMapRecentering") - } - set { - UserDefaults.standard.set(newValue, forKey: "meshMapRecentering") - } - } - static var enableMapNodeHistoryPins: Bool { - get { - UserDefaults.standard.bool(forKey: "meshMapShowNodeHistory") - } - set { - UserDefaults.standard.set(newValue, forKey: "meshMapShowNodeHistory") - } - } - static var enableMapRouteLines: Bool { - get { - UserDefaults.standard.bool(forKey: "meshMapShowRouteLines") - } - set { - UserDefaults.standard.set(newValue, forKey: "meshMapShowRouteLines") - } - } - static var enableMapConvexHull: Bool { - get { - UserDefaults.standard.bool(forKey: "enableMapConvexHull") - } - set { - UserDefaults.standard.set(newValue, forKey: "enableMapConvexHull") - } - } - static var enableMapTraffic: Bool { - get { - UserDefaults.standard.bool(forKey: "enableMapTraffic") - } - set { - UserDefaults.standard.set(newValue, forKey: "enableMapTraffic") - } - } - static var enableMapPointsOfInterest: Bool { - get { - UserDefaults.standard.bool(forKey: "enableMapPointsOfInterest") - } - set { - UserDefaults.standard.set(newValue, forKey: "enableMapPointsOfInterest") - } - } - static var enableOfflineMaps: Bool { - get { - UserDefaults.standard.bool(forKey: "enableOfflineMaps") - } - set { - UserDefaults.standard.set(newValue, forKey: "enableOfflineMaps") - } - } - static var enableOfflineMapsMBTiles: Bool { - get { - UserDefaults.standard.bool(forKey: "enableOfflineMapsMBTiles") - } - set { - UserDefaults.standard.set(newValue, forKey: "enableOfflineMapsMBTiles") - } - } - static var mapTileServer: MapTileServer { - get { - MapTileServer(rawValue: UserDefaults.standard.string(forKey: "mapTileServer") ?? MapTileServer.openStreetMap.rawValue) ?? MapTileServer.openStreetMap - } - set { - UserDefaults.standard.set(newValue.rawValue, forKey: "mapTileServer") - } - } - static var enableOverlayServer: Bool { - get { - UserDefaults.standard.bool(forKey: "enableOverlayServer") - } - set { - UserDefaults.standard.set(newValue, forKey: "enableOverlayServer") - } - } - static var mapOverlayServer: MapOverlayServer { - get { - MapOverlayServer(rawValue: UserDefaults.standard.string(forKey: "mapOverlayServer") ?? MapOverlayServer.baseReReflectivityCurrent.rawValue) ?? MapOverlayServer.baseReReflectivityCurrent - } - set { - UserDefaults.standard.set(newValue.rawValue, forKey: "mapOverlayServer") - } - } - static var mapTilesAboveLabels: Bool { - get { - UserDefaults.standard.bool(forKey: "mapTilesAboveLabels") - } - set { - UserDefaults.standard.set(newValue, forKey: "mapTilesAboveLabels") - } - } - - static var mapUseLegacy: Bool { - get { - UserDefaults.standard.bool(forKey: "mapUseLegacy") - } - set { - UserDefaults.standard.set(newValue, forKey: "mapUseLegacy") - } - } - - static var enableDetectionNotifications: Bool { - get { - UserDefaults.standard.bool(forKey: "enableDetectionNotifications") - } - set { - UserDefaults.standard.set(newValue, forKey: "enableDetectionNotifications") - } - } - - static var detectionSensorRole: DetectionSensorRole { - get { - DetectionSensorRole(rawValue: UserDefaults.standard.string(forKey: "detectionSensorRole") ?? DetectionSensorRole.sensor.rawValue) ?? DetectionSensorRole.sensor - } - set { - UserDefaults.standard.set(newValue.rawValue, forKey: "detectionSensorRole") - } - } - static var enableSmartPosition: Bool { - get { - UserDefaults.standard.bool(forKey: "enableSmartPosition") - } - set { - UserDefaults.standard.set(newValue, forKey: "enableSmartPosition") - } - } - static var modemPreset: Int { - get { - UserDefaults.standard.integer(forKey: "modemPreset") - } - set { - UserDefaults.standard.set(newValue, forKey: "modemPreset") - } - } - static var firmwareVersion: String { - get { - UserDefaults.standard.string(forKey: "firmwareVersion") ?? "0.0.0" - } - set { - UserDefaults.standard.set(newValue, forKey: "firmwareVersion") - } - } + + @UserDefault(.preferredPeripheralId, defaultValue: "") + static var preferredPeripheralId: String + + @UserDefault(.preferredPeripheralNum, defaultValue: 0) + static var preferredPeripheralNum: Int + + @UserDefault(.provideLocation, defaultValue: false) + static var provideLocation: Bool + + @UserDefault(.provideLocationInterval, defaultValue: 0) + static var provideLocationInterval: Int + + @UserDefault(.mapLayer, defaultValue: .standard) + static var mapLayer: MapLayer + + @UserDefault(.enableMapRecentering, defaultValue: false) + static var enableMapRecentering: Bool + + @UserDefault(.enableMapNodeHistoryPins, defaultValue: false) + static var enableMapNodeHistoryPins: Bool + + @UserDefault(.enableMapRouteLines, defaultValue: false) + static var enableMapRouteLines: Bool + + @UserDefault(.enableMapConvexHull, defaultValue: false) + static var enableMapConvexHull: Bool + + @UserDefault(.enableMapTraffic, defaultValue: false) + static var enableMapTraffic: Bool + + @UserDefault(.enableMapPointsOfInterest, defaultValue: false) + static var enableMapPointsOfInterest: Bool + + @UserDefault(.enableOfflineMaps, defaultValue: false) + static var enableOfflineMaps: Bool + + @UserDefault(.enableOfflineMapsMBTiles, defaultValue: false) + static var enableOfflineMapsMBTiles: Bool + + @UserDefault(.mapTileServer, defaultValue: .openStreetMap) + static var mapTileServer: MapTileServer + + @UserDefault(.enableOverlayServer, defaultValue: false) + static var enableOverlayServer: Bool + + @UserDefault(.mapOverlayServer, defaultValue: .baseReReflectivityCurrent) + static var mapOverlayServer: MapOverlayServer + + @UserDefault(.mapTilesAboveLabels, defaultValue: false) + static var mapTilesAboveLabels: Bool + + @UserDefault(.mapUseLegacy, defaultValue: false) + static var mapUseLegacy: Bool + + @UserDefault(.enableDetectionNotifications, defaultValue: false) + static var enableDetectionNotifications: Bool + + @UserDefault(.detectionSensorRole, defaultValue: .sensor) + static var detectionSensorRole: DetectionSensorRole + + @UserDefault(.enableSmartPosition, defaultValue: false) + static var enableSmartPosition: Bool + + @UserDefault(.modemPreset, defaultValue: 0) + static var modemPreset: Int + + @UserDefault(.firmwareVersion, defaultValue: "0.0.0") + static var firmwareVersion: String } From 7d00a5882b9c7173978ab39d38244ab74c5a5c1f Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Sat, 23 Mar 2024 18:01:20 -0700 Subject: [PATCH 04/34] Try and use less ram on the mesh map --- .../CoreData/PositionEntityExtension.swift | 7 +- .../CoreData/WaypointEntityExtension.swift | 10 ++ .../contents | 7 +- .../Map/MapContent/MeshMapContent.swift | 25 ++-- Meshtastic/Views/Nodes/MeshMap.swift | 117 ++++++------------ 5 files changed, 66 insertions(+), 100 deletions(-) diff --git a/Meshtastic/Extensions/CoreData/PositionEntityExtension.swift b/Meshtastic/Extensions/CoreData/PositionEntityExtension.swift index 4b9ce0c2..1d1fd991 100644 --- a/Meshtastic/Extensions/CoreData/PositionEntityExtension.swift +++ b/Meshtastic/Extensions/CoreData/PositionEntityExtension.swift @@ -14,11 +14,12 @@ extension PositionEntity { static func allPositionsFetchRequest() -> NSFetchRequest { let request: NSFetchRequest = PositionEntity.fetchRequest() - // request.fetchLimit = 100 - //request.fetchBatchSize = 2 + request.fetchLimit = 75 + //request.fetchBatchSize = 1 + //request.returnsObjectsAsFaults = true //request.includesSubentities = false request.returnsDistinctResults = true - request.sortDescriptors = [NSSortDescriptor(key: "time", ascending: true)] + request.sortDescriptors = [NSSortDescriptor(key: "time", ascending: false)] request.predicate = NSPredicate(format: "nodePosition != nil && latest == true", Calendar.current.date(byAdding: .day, value: -7, to: Date())! as NSDate) return request } diff --git a/Meshtastic/Extensions/CoreData/WaypointEntityExtension.swift b/Meshtastic/Extensions/CoreData/WaypointEntityExtension.swift index 968381b2..76fe8f8e 100644 --- a/Meshtastic/Extensions/CoreData/WaypointEntityExtension.swift +++ b/Meshtastic/Extensions/CoreData/WaypointEntityExtension.swift @@ -10,6 +10,16 @@ import MapKit import SwiftUI extension WaypointEntity { + + static func allWaypointssFetchRequest() -> NSFetchRequest { + let request: NSFetchRequest = WaypointEntity.fetchRequest() + //request.fetchLimit = 100 + //request.fetchBatchSize = 1 + request.returnsDistinctResults = true + request.sortDescriptors = [NSSortDescriptor(key: "name", ascending: false)] + request.predicate = NSPredicate(format: "expire == nil || expire >= %@", Date() as NSDate) + return request + } var latitude: Double? { diff --git a/Meshtastic/Meshtastic.xcdatamodeld/MeshtasticDataModelV 30.xcdatamodel/contents b/Meshtastic/Meshtastic.xcdatamodeld/MeshtasticDataModelV 30.xcdatamodel/contents index 733896d1..72ba98d4 100644 --- a/Meshtastic/Meshtastic.xcdatamodeld/MeshtasticDataModelV 30.xcdatamodel/contents +++ b/Meshtastic/Meshtastic.xcdatamodeld/MeshtasticDataModelV 30.xcdatamodel/contents @@ -1,5 +1,5 @@ - + @@ -448,5 +448,10 @@ + + + + + \ No newline at end of file diff --git a/Meshtastic/Views/Nodes/Helpers/Map/MapContent/MeshMapContent.swift b/Meshtastic/Views/Nodes/Helpers/Map/MapContent/MeshMapContent.swift index 0e0c519f..8b3440ac 100644 --- a/Meshtastic/Views/Nodes/Helpers/Map/MapContent/MeshMapContent.swift +++ b/Meshtastic/Views/Nodes/Helpers/Map/MapContent/MeshMapContent.swift @@ -26,13 +26,6 @@ struct MeshMapContent: MapContent { @Binding var showPointsOfInterest: Bool @Binding var selectedMapLayer: MapLayer // Map Configuration - ///@Namespace var mapScope - - @State var mapStyle: MapStyle = MapStyle.standard(elevation: .realistic, emphasis: MapStyle.StandardEmphasis.muted ,pointsOfInterest: .excludingAll, showsTraffic: false) - //@State var position = MapCameraPosition.automatic - //@State var scene: MKLookAroundScene? - //@State var isLookingAround = false - //@State var isEditingSettings = false @Binding var selectedPosition: PositionEntity? @Binding var showWaypoints: Bool //@Binding var editingWaypoint: WaypointEntity? @@ -67,15 +60,15 @@ struct MeshMapContent: MapContent { Circle() .fill(Color(nodeColor.lighter()).opacity(0.4).shadow(.drop(color: Color(nodeColor).isLight() ? .black : .white, radius: 5))) .foregroundStyle(Color(nodeColor.lighter()).opacity(0.3)) -// .scaleEffect(scale) -// .animation( -// Animation.easeInOut(duration: 0.6) -// .repeatForever().delay(delay), value: scale -// ) -// .onAppear { -// self.scale = 1 -// } -// .frame(width: 60, height: 60) + .scaleEffect(scale) + .animation( + Animation.easeInOut(duration: 0.6) + .repeatForever().delay(delay), value: scale + ) + .onAppear { + self.scale = 1 + } + .frame(width: 60, height: 60) } if position.nodePosition?.hasDetectionSensorMetrics ?? false { Image(systemName: "sensor.fill") diff --git a/Meshtastic/Views/Nodes/MeshMap.swift b/Meshtastic/Views/Nodes/MeshMap.swift index ddc66314..47d83edb 100644 --- a/Meshtastic/Views/Nodes/MeshMap.swift +++ b/Meshtastic/Views/Nodes/MeshMap.swift @@ -32,44 +32,22 @@ struct MeshMap: View { @AppStorage("mapLayer") private var selectedMapLayer: MapLayer = .hybrid // Map Configuration @Namespace var mapScope - @State var mapStyle: MapStyle = MapStyle.standard(elevation: .realistic, emphasis: MapStyle.StandardEmphasis.muted ,pointsOfInterest: .all, showsTraffic: true) + @State var mapStyle: MapStyle = MapStyle.standard(elevation: .flat, emphasis: MapStyle.StandardEmphasis.muted ,pointsOfInterest: .all, showsTraffic: true) @State var position = MapCameraPosition.automatic - //@State var scene: MKLookAroundScene? - //@State var isLookingAround = false @State var isEditingSettings = false @State var selectedPosition: PositionEntity? - @State var showWaypoints = true + @State var showWaypoints = false @State var editingWaypoint: WaypointEntity? @State var selectedWaypoint: WaypointEntity? @State var newWaypointCoord: CLLocationCoordinate2D? @State var isMeshMap = true - let positionRequest: NSFetchRequest = { - // Create a fetch request. - let request = PositionEntity.fetchRequest() - - // Limit the maximum number of items that the request returns. - request.fetchLimit = 100 - - // Filter the request results, such as to only return unchecked items. - request.predicate = NSPredicate(format: "nodePosition != nil && latest == true && time >= %@", Calendar.current.date(byAdding: .hour, value: -6, to: Date())! as NSDate) - - // Sort the fetched results - request.sortDescriptors = [NSSortDescriptor(key: "time", ascending: true)] - - - return request - }() - /// && time >= %@ @FetchRequest(fetchRequest: PositionEntity.allPositionsFetchRequest(), animation: .none) var positions: FetchedResults - @FetchRequest(sortDescriptors: [NSSortDescriptor(key: "name", ascending: false)], - predicate: NSPredicate( - format: "expire == nil || expire >= %@", Date() as NSDate - ), animation: .none) - private var waypoints: FetchedResults + @FetchRequest(fetchRequest: WaypointEntity.allWaypointssFetchRequest(), animation: .none) + var waypoints: FetchedResults @FetchRequest(sortDescriptors: [NSSortDescriptor(key: "name", ascending: true)], predicate: NSPredicate(format: "enabled == true", ""), animation: .none) @@ -82,41 +60,7 @@ struct MeshMap: View { MapReader { reader in Map(position: $position, bounds: MapCameraBounds(minimumDistance: 1, maximumDistance: .infinity), scope: mapScope) { MeshMapContent(positions: Array(positions), waypoints: Array(waypoints), routes: Array(routes), showUserLocation: $showUserLocation, showNodeHistory: $showNodeHistory, showRouteLines: $showRouteLines, showConvexHull: $showConvexHull, showTraffic: $showTraffic, showPointsOfInterest: $showPointsOfInterest, selectedMapLayer: $selectedMapLayer, selectedPosition: $selectedPosition, showWaypoints: $showWaypoints, selectedWaypoint: $selectedWaypoint) - -// /// Routes -// ForEach(Array(routes), id: \.id) { route in -// let routeLocations = Array(route.locations!) as! [LocationEntity] -// let routeCoords = routeLocations.compactMap({(loc) -> CLLocationCoordinate2D in -// return loc.locationCoordinate ?? LocationHelper.DefaultLocation -// }) -// Annotation("Start", coordinate: routeCoords.first ?? LocationHelper.DefaultLocation) { -// ZStack { -// Circle() -// .fill(Color(.green)) -// .strokeBorder(.white, lineWidth: 3) -// .frame(width: 15, height: 15) -// } -// } -// .annotationTitles(.automatic) -// Annotation("Finish", coordinate: routeCoords.last ?? LocationHelper.DefaultLocation) { -// ZStack { -// Circle() -// .fill(Color(.black)) -// .strokeBorder(.white, lineWidth: 3) -// .frame(width: 15, height: 15) -// } -// } -// .annotationTitles(.automatic) -// let solid = StrokeStyle( -// lineWidth: 3, -// lineCap: .round, lineJoin: .round -// ) -// MapPolyline(coordinates: routeCoords) -// .stroke(Color(UIColor(hex: UInt32(route.color))), style: solid) -// -// } -// } -// + } .mapScope(mapScope) .mapStyle(mapStyle) @@ -129,28 +73,41 @@ struct MeshMap: View { .mapControlVisibility(.automatic) } .controlSize(.regular) - .onTapGesture(count: 1, perform: { - position in - print(position) - // tapText = "map tap" - newWaypointCoord = reader.convert(position, from: .local) ?? CLLocationCoordinate2D.init() - }) - .onTapGesture(count: 1, perform: { location in - // newWaypointCoord = reader.convert(location , from: .local) + + .onTapGesture(count: 1, perform: { position in + print(position) + newWaypointCoord = reader.convert(position, from: .local) ?? CLLocationCoordinate2D.init() + }) + .gesture( + LongPressGesture(minimumDuration: 0.5) + .sequenced(before: SpatialTapGesture(coordinateSpace: .local)) + .onEnded { value in + switch value { + case let .second(_, tapValue): + guard let point = tapValue?.location else { + print("Unable to retreive tap location from gesture data.") + return + } + + guard let coordinate = reader.convert(point, from: .local) else { + print("Unable to convert local point to coordinate on map.") + return + } + + newWaypointCoord = coordinate + editingWaypoint = WaypointEntity(context: context) + editingWaypoint!.name = "Waypoint Pin" + editingWaypoint!.expire = Date.now.addingTimeInterval(60 * 480) + editingWaypoint!.latitudeI = Int32((newWaypointCoord?.latitude ?? 0) * 1e7) + editingWaypoint!.longitudeI = Int32((newWaypointCoord?.longitude ?? 0) * 1e7) + editingWaypoint!.expire = Date.now.addingTimeInterval(60 * 480) + editingWaypoint!.id = 0 + print("Long press occured at: \(coordinate)") + default: return + } }) - .onLongPressGesture(minimumDuration: 0.5, maximumDistance: 10) { - editingWaypoint = WaypointEntity(context: context) - editingWaypoint!.name = "Waypoint Pin" - editingWaypoint!.expire = Date.now.addingTimeInterval(60 * 480) - editingWaypoint!.latitudeI = Int32((newWaypointCoord?.latitude ?? 0) * 1e7) - editingWaypoint!.longitudeI = Int32((newWaypointCoord?.longitude ?? 0) * 1e7) - editingWaypoint!.expire = Date.now.addingTimeInterval(60 * 480) - editingWaypoint!.id = 0 - } - } } - .sheet(item: $selectedPosition) { selection in PositionPopover(position: selection, popover: false) .padding() From 03a210e415646ca99f0b8911fca78c11cbf68ffe Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Sat, 23 Mar 2024 18:22:10 -0700 Subject: [PATCH 05/34] Get the last 50 nodes seen for the mesh map, zoom much closer by default, only show nodes from the last 48 hours on the mesh map --- Meshtastic/Extensions/CoreData/PositionEntityExtension.swift | 4 ++-- Meshtastic/Views/Nodes/MeshMap.swift | 5 ++--- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/Meshtastic/Extensions/CoreData/PositionEntityExtension.swift b/Meshtastic/Extensions/CoreData/PositionEntityExtension.swift index 1d1fd991..79d4f581 100644 --- a/Meshtastic/Extensions/CoreData/PositionEntityExtension.swift +++ b/Meshtastic/Extensions/CoreData/PositionEntityExtension.swift @@ -14,13 +14,13 @@ extension PositionEntity { static func allPositionsFetchRequest() -> NSFetchRequest { let request: NSFetchRequest = PositionEntity.fetchRequest() - request.fetchLimit = 75 + request.fetchLimit = 50 //request.fetchBatchSize = 1 //request.returnsObjectsAsFaults = true //request.includesSubentities = false request.returnsDistinctResults = true request.sortDescriptors = [NSSortDescriptor(key: "time", ascending: false)] - request.predicate = NSPredicate(format: "nodePosition != nil && latest == true", Calendar.current.date(byAdding: .day, value: -7, to: Date())! as NSDate) + request.predicate = NSPredicate(format: "nodePosition != nil && latest == true && time >= %@", Calendar.current.date(byAdding: .day, value: -2, to: Date())! as NSDate) return request } diff --git a/Meshtastic/Views/Nodes/MeshMap.swift b/Meshtastic/Views/Nodes/MeshMap.swift index 47d83edb..d17c24cd 100644 --- a/Meshtastic/Views/Nodes/MeshMap.swift +++ b/Meshtastic/Views/Nodes/MeshMap.swift @@ -29,11 +29,11 @@ struct MeshMap: View { @AppStorage("enableMapConvexHull") private var showConvexHull = false @AppStorage("enableMapTraffic") private var showTraffic: Bool = false @AppStorage("enableMapPointsOfInterest") private var showPointsOfInterest: Bool = false - @AppStorage("mapLayer") private var selectedMapLayer: MapLayer = .hybrid + @AppStorage("mapLayer") private var selectedMapLayer: MapLayer = .standard // Map Configuration @Namespace var mapScope @State var mapStyle: MapStyle = MapStyle.standard(elevation: .flat, emphasis: MapStyle.StandardEmphasis.muted ,pointsOfInterest: .all, showsTraffic: true) - @State var position = MapCameraPosition.automatic + @State var position = MapCameraPosition.camera(MapCamera(centerCoordinate: LocationHelper.currentLocation, distance: 2500000, heading: 0, pitch: 0)) @State var isEditingSettings = false @State var selectedPosition: PositionEntity? @State var showWaypoints = false @@ -42,7 +42,6 @@ struct MeshMap: View { @State var newWaypointCoord: CLLocationCoordinate2D? @State var isMeshMap = true - /// && time >= %@ @FetchRequest(fetchRequest: PositionEntity.allPositionsFetchRequest(), animation: .none) var positions: FetchedResults From e9e886e5bc58efbf05865da994e0581880208c15 Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Sun, 24 Mar 2024 11:45:49 -0700 Subject: [PATCH 06/34] Filter out far away positions on the node map --- .../CoreData/PositionEntityExtension.swift | 26 ++++++++++++++++--- 1 file changed, 23 insertions(+), 3 deletions(-) diff --git a/Meshtastic/Extensions/CoreData/PositionEntityExtension.swift b/Meshtastic/Extensions/CoreData/PositionEntityExtension.swift index 79d4f581..9e9a6c49 100644 --- a/Meshtastic/Extensions/CoreData/PositionEntityExtension.swift +++ b/Meshtastic/Extensions/CoreData/PositionEntityExtension.swift @@ -14,13 +14,33 @@ extension PositionEntity { static func allPositionsFetchRequest() -> NSFetchRequest { let request: NSFetchRequest = PositionEntity.fetchRequest() - request.fetchLimit = 50 + request.fetchLimit = 200 //request.fetchBatchSize = 1 - //request.returnsObjectsAsFaults = true + request.returnsObjectsAsFaults = false //request.includesSubentities = false request.returnsDistinctResults = true request.sortDescriptors = [NSSortDescriptor(key: "time", ascending: false)] - request.predicate = NSPredicate(format: "nodePosition != nil && latest == true && time >= %@", Calendar.current.date(byAdding: .day, value: -2, to: Date())! as NSDate) + + let positionPredicate = NSPredicate(format: "nodePosition != nil && latest == true && time >= %@", Calendar.current.date(byAdding: .day, value: -2, to: Date())! as NSDate) + + let pointOfInterest = LocationHelper.currentLocation + + if pointOfInterest.latitude != LocationHelper.DefaultLocation.latitude && pointOfInterest.longitude != LocationHelper.DefaultLocation.longitude { + /// Lets just get nodes within about 500 miles + let D: Double = 800000 * 1.1 + let R: Double = 6371009 + let meanLatitidue = pointOfInterest.latitude * .pi / 180 + let deltaLatitude = D / R * 180 / .pi + let deltaLongitude = D / (R * cos(meanLatitidue)) * 180 / .pi + let minLatitude: Double = pointOfInterest.latitude - deltaLatitude + let maxLatitude: Double = pointOfInterest.latitude + deltaLatitude + let minLongitude: Double = pointOfInterest.longitude - deltaLongitude + let maxLongitude: Double = pointOfInterest.longitude + deltaLongitude + let distancePredicate = NSPredicate(format: "(%lf <= (longitudeI / 1e7)) AND ((longitudeI / 1e7) <= %lf) AND (%lf <= (latitudeI / 1e7)) AND ((latitudeI / 1e7) <= %lf)", minLongitude, maxLongitude,minLatitude, maxLatitude) + request.predicate = NSCompoundPredicate(type: .and, subpredicates: [distancePredicate, positionPredicate]) + } else { + request.predicate = positionPredicate + } return request } From 6be729305d52cfe137932ebf712289fee71bd4f5 Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Sun, 24 Mar 2024 13:13:05 -0700 Subject: [PATCH 07/34] All positions query updates --- .../Extensions/CoreData/PositionEntityExtension.swift | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Meshtastic/Extensions/CoreData/PositionEntityExtension.swift b/Meshtastic/Extensions/CoreData/PositionEntityExtension.swift index 9e9a6c49..5d5e2fa5 100644 --- a/Meshtastic/Extensions/CoreData/PositionEntityExtension.swift +++ b/Meshtastic/Extensions/CoreData/PositionEntityExtension.swift @@ -14,14 +14,14 @@ extension PositionEntity { static func allPositionsFetchRequest() -> NSFetchRequest { let request: NSFetchRequest = PositionEntity.fetchRequest() - request.fetchLimit = 200 + //request.fetchLimit = 200 //request.fetchBatchSize = 1 request.returnsObjectsAsFaults = false - //request.includesSubentities = false + request.includesSubentities = true request.returnsDistinctResults = true request.sortDescriptors = [NSSortDescriptor(key: "time", ascending: false)] - let positionPredicate = NSPredicate(format: "nodePosition != nil && latest == true && time >= %@", Calendar.current.date(byAdding: .day, value: -2, to: Date())! as NSDate) + let positionPredicate = NSPredicate(format: "nodePosition != nil && nodePosition.user.shortName != nil && latest == true && time >= %@", Calendar.current.date(byAdding: .day, value: -2, to: Date())! as NSDate) let pointOfInterest = LocationHelper.currentLocation @@ -37,7 +37,7 @@ extension PositionEntity { let minLongitude: Double = pointOfInterest.longitude - deltaLongitude let maxLongitude: Double = pointOfInterest.longitude + deltaLongitude let distancePredicate = NSPredicate(format: "(%lf <= (longitudeI / 1e7)) AND ((longitudeI / 1e7) <= %lf) AND (%lf <= (latitudeI / 1e7)) AND ((latitudeI / 1e7) <= %lf)", minLongitude, maxLongitude,minLatitude, maxLatitude) - request.predicate = NSCompoundPredicate(type: .and, subpredicates: [distancePredicate, positionPredicate]) + request.predicate = NSCompoundPredicate(type: .and, subpredicates: [positionPredicate, distancePredicate]) } else { request.predicate = positionPredicate } From 554fe9a3ca0b9fbd1b04e3db2a3cf2f7ab32bad6 Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Sun, 24 Mar 2024 22:23:55 -0700 Subject: [PATCH 08/34] Node distance filter for the mesh map --- Meshtastic/Enums/AppSettingsEnums.swift | 14 +++++++++++ .../CoreData/PositionEntityExtension.swift | 4 +-- Meshtastic/Extensions/String.swift | 25 +++++++++++++++++++ Meshtastic/Extensions/UserDefaults.swift | 4 +++ Meshtastic/Helpers/MeshPackets.swift | 13 +++++++--- .../Nodes/Helpers/Map/MapSettingsForm.swift | 22 ++++++++++++++++ .../Nodes/Helpers/Map/NodeMapSwiftUI.swift | 3 ++- Meshtastic/Views/Nodes/MeshMap.swift | 7 +++--- .../Views/Settings/Channels/ChannelForm.swift | 2 +- 9 files changed, 83 insertions(+), 11 deletions(-) diff --git a/Meshtastic/Enums/AppSettingsEnums.swift b/Meshtastic/Enums/AppSettingsEnums.swift index dd0a94ed..fd4fe088 100644 --- a/Meshtastic/Enums/AppSettingsEnums.swift +++ b/Meshtastic/Enums/AppSettingsEnums.swift @@ -50,6 +50,20 @@ enum MeshMapTypes: Int, CaseIterable, Identifiable { } } +enum MeshMapDistances: Double, CaseIterable, Identifiable { + case fiftyMiles = 80467.2 + case oneHundredMiles = 160934 + case twoHundredMiles = 321869 + case fiveHundredMiles = 804672 + case oneThousandMiles = 1609000 + case twoThousandMiles = 3218688 + var id: Double { self.rawValue } + var description: String { + let distanceFormatter = MKDistanceFormatter() + return "\(distanceFormatter.string(fromDistance: Double(self.rawValue))) away" + } +} + enum UserTrackingModes: Int, CaseIterable, Identifiable { case none = 0 case follow = 1 diff --git a/Meshtastic/Extensions/CoreData/PositionEntityExtension.swift b/Meshtastic/Extensions/CoreData/PositionEntityExtension.swift index 5d5e2fa5..44777bb3 100644 --- a/Meshtastic/Extensions/CoreData/PositionEntityExtension.swift +++ b/Meshtastic/Extensions/CoreData/PositionEntityExtension.swift @@ -21,13 +21,13 @@ extension PositionEntity { request.returnsDistinctResults = true request.sortDescriptors = [NSSortDescriptor(key: "time", ascending: false)] - let positionPredicate = NSPredicate(format: "nodePosition != nil && nodePosition.user.shortName != nil && latest == true && time >= %@", Calendar.current.date(byAdding: .day, value: -2, to: Date())! as NSDate) + let positionPredicate = NSPredicate(format: "nodePosition != nil && (nodePosition.user.shortName != nil || nodePosition.user.shortName != '') && latest == true && time >= %@", Calendar.current.date(byAdding: .day, value: -2, to: Date())! as NSDate) let pointOfInterest = LocationHelper.currentLocation if pointOfInterest.latitude != LocationHelper.DefaultLocation.latitude && pointOfInterest.longitude != LocationHelper.DefaultLocation.longitude { /// Lets just get nodes within about 500 miles - let D: Double = 800000 * 1.1 + let D: Double = UserDefaults.meshMapDistance * 1.1 let R: Double = 6371009 let meanLatitidue = pointOfInterest.latitude * .pi / 180 let deltaLatitude = D / R * 180 / .pi diff --git a/Meshtastic/Extensions/String.swift b/Meshtastic/Extensions/String.swift index 6ac1672a..6255b151 100644 --- a/Meshtastic/Extensions/String.swift +++ b/Meshtastic/Extensions/String.swift @@ -67,4 +67,29 @@ extension String { : $0 + String($1) } } + + var length: Int { + return count + } + + subscript (i: Int) -> String { + return self[i ..< i + 1] + } + + func substring(fromIndex: Int) -> String { + return self[min(fromIndex, length) ..< length] + } + + func substring(toIndex: Int) -> String { + return self[0 ..< max(0, toIndex)] + } + + subscript (r: Range) -> String { + let range = Range(uncheckedBounds: (lower: max(0, min(length, r.lowerBound)), + upper: min(length, max(0, r.upperBound)))) + let start = index(startIndex, offsetBy: range.lowerBound) + let end = index(start, offsetBy: range.upperBound - range.lowerBound) + return String(self[start ..< end]) + } + } diff --git a/Meshtastic/Extensions/UserDefaults.swift b/Meshtastic/Extensions/UserDefaults.swift index c42ee9bc..b497495d 100644 --- a/Meshtastic/Extensions/UserDefaults.swift +++ b/Meshtastic/Extensions/UserDefaults.swift @@ -34,6 +34,7 @@ extension UserDefaults { case provideLocation case provideLocationInterval case mapLayer + case meshMapDistance case meshMapRecentering case meshMapShowNodeHistory case meshMapShowRouteLines @@ -76,6 +77,9 @@ extension UserDefaults { @UserDefault(.mapLayer, defaultValue: .standard) static var mapLayer: MapLayer + @UserDefault(.meshMapDistance, defaultValue: 800000) + static var meshMapDistance: Double + @UserDefault(.enableMapRecentering, defaultValue: false) static var enableMapRecentering: Bool diff --git a/Meshtastic/Helpers/MeshPackets.swift b/Meshtastic/Helpers/MeshPackets.swift index e89aeb82..2e1e31cf 100644 --- a/Meshtastic/Helpers/MeshPackets.swift +++ b/Meshtastic/Helpers/MeshPackets.swift @@ -15,7 +15,7 @@ import ActivityKit func generateMessageMarkdown (message: String) -> String { let types: NSTextCheckingResult.CheckingType = [.address, .link, .phoneNumber] let detector = try! NSDataDetector(types: types.rawValue) - let matches = detector.matches(in: message, options: [], range: NSRange(location: 0, length: message.utf16.count)) + let matches = detector.matches(in: message, options: [], range: NSRange(location: 0, length: message.utf8.count)) var messageWithMarkdown = message if matches.count > 0 { for match in matches { @@ -28,9 +28,14 @@ func generateMessageMarkdown (message: String) -> String { let phone = messageWithMarkdown[range] messageWithMarkdown = messageWithMarkdown.replacingOccurrences(of: phone, with: "[\(phone)](tel:\(phone))") } else if match.resultType == .link { - let url = messageWithMarkdown[range] - let absoluteUrl = match.url?.absoluteString ?? "" - messageWithMarkdown = messageWithMarkdown.replacingOccurrences(of: url, with: "[\(String(match.url?.host ?? "Link"))\(String(match.url?.path ?? ""))](\(absoluteUrl))") + let start = match.range.lowerBound + let stop = match.range.upperBound + if stop > start { + let url = message[start ..< stop] + let absoluteUrl = match.url?.absoluteString ?? "" + let markdownUrl = "[\(url)](\(absoluteUrl))" + messageWithMarkdown = messageWithMarkdown.replacingOccurrences(of: url, with: markdownUrl) + } } } } diff --git a/Meshtastic/Views/Nodes/Helpers/Map/MapSettingsForm.swift b/Meshtastic/Views/Nodes/Helpers/Map/MapSettingsForm.swift index 4c18ef19..55b11484 100644 --- a/Meshtastic/Views/Nodes/Helpers/Map/MapSettingsForm.swift +++ b/Meshtastic/Views/Nodes/Helpers/Map/MapSettingsForm.swift @@ -19,6 +19,7 @@ struct MapSettingsForm: View { @Binding var traffic: Bool @Binding var pointsOfInterest: Bool @Binding var mapLayer: MapLayer + @Binding var meshMapDistance: Double @Binding var meshMap: Bool var body: some View { @@ -56,6 +57,27 @@ struct MapSettingsForm: View { self.routeLines.toggle() UserDefaults.enableMapRouteLines = self.routeLines } + } else { + VStack { + HStack { + Label("Show nodes up to", systemImage: "lines.measurement.horizontal") + Picker("", selection: $meshMapDistance) { + ForEach(MeshMapDistances.allCases) { di in + Text(di.description) + .tag(di.id) + } + } + .pickerStyle(DefaultPickerStyle()) + } + .listRowSeparator(.hidden) + Text("You will need to close and re-open the app for this to take effect.") + .font(.callout) + .foregroundColor(.gray) + .listRowSeparator(/*@START_MENU_TOKEN@*/.visible/*@END_MENU_TOKEN@*/) + } + .onChange(of: meshMapDistance) { newMeshMapDistance in + UserDefaults.meshMapDistance = newMeshMapDistance + } } Toggle(isOn: $convexHull) { Label("Convex Hull", systemImage: "button.angledbottom.horizontal.right") diff --git a/Meshtastic/Views/Nodes/Helpers/Map/NodeMapSwiftUI.swift b/Meshtastic/Views/Nodes/Helpers/Map/NodeMapSwiftUI.swift index cf6d90e3..3f2fb0fb 100644 --- a/Meshtastic/Views/Nodes/Helpers/Map/NodeMapSwiftUI.swift +++ b/Meshtastic/Views/Nodes/Helpers/Map/NodeMapSwiftUI.swift @@ -20,6 +20,7 @@ struct NodeMapSwiftUI: View { @State var showUserLocation: Bool = false @State var positions: [PositionEntity] = [] /// Map State User Defaults + @AppStorage("meshMapDistance") private var meshMapDistance: Double = 800000 @AppStorage("meshMapShowNodeHistory") private var showNodeHistory = false @AppStorage("meshMapShowRouteLines") private var showRouteLines = false @AppStorage("enableMapConvexHull") private var showConvexHull = false @@ -92,7 +93,7 @@ struct NodeMapSwiftUI: View { .padding() } .sheet(isPresented: $isEditingSettings) { - MapSettingsForm(nodeHistory: $showNodeHistory, routeLines: $showRouteLines, convexHull: $showConvexHull, traffic: $showTraffic, pointsOfInterest: $showPointsOfInterest, mapLayer: $selectedMapLayer, meshMap: $isMeshMap) + MapSettingsForm(nodeHistory: $showNodeHistory, routeLines: $showRouteLines, convexHull: $showConvexHull, traffic: $showTraffic, pointsOfInterest: $showPointsOfInterest, mapLayer: $selectedMapLayer, meshMapDistance: $meshMapDistance, meshMap: $isMeshMap) .onChange(of: (selectedMapLayer)) { newMapLayer in switch selectedMapLayer { case .standard: diff --git a/Meshtastic/Views/Nodes/MeshMap.swift b/Meshtastic/Views/Nodes/MeshMap.swift index d17c24cd..780ebde2 100644 --- a/Meshtastic/Views/Nodes/MeshMap.swift +++ b/Meshtastic/Views/Nodes/MeshMap.swift @@ -24,6 +24,7 @@ struct MeshMap: View { /// Parameters @State var showUserLocation: Bool = true /// Map State User Defaults + @AppStorage("meshMapDistance") private var meshMapDistance: Double = 800000 @AppStorage("meshMapShowNodeHistory") private var showNodeHistory = false @AppStorage("meshMapShowRouteLines") private var showRouteLines = false @AppStorage("enableMapConvexHull") private var showConvexHull = false @@ -32,8 +33,8 @@ struct MeshMap: View { @AppStorage("mapLayer") private var selectedMapLayer: MapLayer = .standard // Map Configuration @Namespace var mapScope - @State var mapStyle: MapStyle = MapStyle.standard(elevation: .flat, emphasis: MapStyle.StandardEmphasis.muted ,pointsOfInterest: .all, showsTraffic: true) - @State var position = MapCameraPosition.camera(MapCamera(centerCoordinate: LocationHelper.currentLocation, distance: 2500000, heading: 0, pitch: 0)) + @State var mapStyle: MapStyle = MapStyle.standard(elevation: .flat, emphasis: MapStyle.StandardEmphasis.muted ,pointsOfInterest: .excludingAll, showsTraffic: false) + @State var position = MapCameraPosition.automatic @State var isEditingSettings = false @State var selectedPosition: PositionEntity? @State var showWaypoints = false @@ -120,7 +121,7 @@ struct MeshMap: View { .padding() } .sheet(isPresented: $isEditingSettings) { - MapSettingsForm(nodeHistory: $showNodeHistory, routeLines: $showRouteLines, convexHull: $showConvexHull, traffic: $showTraffic, pointsOfInterest: $showPointsOfInterest, mapLayer: $selectedMapLayer, meshMap: $isMeshMap) + MapSettingsForm(nodeHistory: $showNodeHistory, routeLines: $showRouteLines, convexHull: $showConvexHull, traffic: $showTraffic, pointsOfInterest: $showPointsOfInterest, mapLayer: $selectedMapLayer, meshMapDistance: $meshMapDistance, meshMap: $isMeshMap) } .onChange(of: (appState.navigationPath)) { newPath in diff --git a/Meshtastic/Views/Settings/Channels/ChannelForm.swift b/Meshtastic/Views/Settings/Channels/ChannelForm.swift index 7176d802..da6127df 100644 --- a/Meshtastic/Views/Settings/Channels/ChannelForm.swift +++ b/Meshtastic/Views/Settings/Channels/ChannelForm.swift @@ -245,7 +245,7 @@ struct ChannelForm: View { } } } - .presentationDetents([.fraction(0.45), .fraction(0.65)]) + .presentationDetents([.large]) .presentationDragIndicator(.visible) } } From d768eb64e9304e34750fbe2ad5bce9006a48b49f Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Sun, 24 Mar 2024 22:31:47 -0700 Subject: [PATCH 09/34] Dont allow json and the client proxy, max position request of 300 for the mesh map --- .../Extensions/CoreData/PositionEntityExtension.swift | 2 +- Meshtastic/Views/Settings/Config/Module/MQTTConfig.swift | 6 ++++++ 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/Meshtastic/Extensions/CoreData/PositionEntityExtension.swift b/Meshtastic/Extensions/CoreData/PositionEntityExtension.swift index 44777bb3..20fbbd35 100644 --- a/Meshtastic/Extensions/CoreData/PositionEntityExtension.swift +++ b/Meshtastic/Extensions/CoreData/PositionEntityExtension.swift @@ -14,7 +14,7 @@ extension PositionEntity { static func allPositionsFetchRequest() -> NSFetchRequest { let request: NSFetchRequest = PositionEntity.fetchRequest() - //request.fetchLimit = 200 + request.fetchLimit = 300 //request.fetchBatchSize = 1 request.returnsObjectsAsFaults = false request.includesSubentities = true diff --git a/Meshtastic/Views/Settings/Config/Module/MQTTConfig.swift b/Meshtastic/Views/Settings/Config/Module/MQTTConfig.swift index 3d228ad3..cfcaa930 100644 --- a/Meshtastic/Views/Settings/Config/Module/MQTTConfig.swift +++ b/Meshtastic/Views/Settings/Config/Module/MQTTConfig.swift @@ -276,6 +276,9 @@ struct MQTTConfig: View { } } .onChange(of: proxyToClientEnabled) { newProxyToClientEnabled in + if newProxyToClientEnabled { + jsonEnabled = false + } if node != nil && node?.mqttConfig != nil { if newProxyToClientEnabled != node!.mqttConfig!.proxyToClientEnabled { hasChanges = true } if newProxyToClientEnabled { @@ -289,6 +292,9 @@ struct MQTTConfig: View { } } .onChange(of: jsonEnabled) { newJsonEnabled in + if newJsonEnabled { + proxyToClientEnabled = false + } if node != nil && node?.mqttConfig != nil { if newJsonEnabled != node!.mqttConfig!.jsonEnabled { hasChanges = true } } From c372a8e940254525f96738ab1935acfbef7ad1d7 Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Sun, 24 Mar 2024 22:37:50 -0700 Subject: [PATCH 10/34] Shorten label for mesh map node distance --- Meshtastic/Views/Nodes/Helpers/Map/MapSettingsForm.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Meshtastic/Views/Nodes/Helpers/Map/MapSettingsForm.swift b/Meshtastic/Views/Nodes/Helpers/Map/MapSettingsForm.swift index 55b11484..58405043 100644 --- a/Meshtastic/Views/Nodes/Helpers/Map/MapSettingsForm.swift +++ b/Meshtastic/Views/Nodes/Helpers/Map/MapSettingsForm.swift @@ -60,7 +60,7 @@ struct MapSettingsForm: View { } else { VStack { HStack { - Label("Show nodes up to", systemImage: "lines.measurement.horizontal") + Label("Show nodes", systemImage: "lines.measurement.horizontal") Picker("", selection: $meshMapDistance) { ForEach(MeshMapDistances.allCases) { di in Text(di.description) From f3f126da38ead8ea6da996d9d8c06761a76599d0 Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Sun, 24 Mar 2024 22:56:38 -0700 Subject: [PATCH 11/34] Add node history and route lines back to the mesh map for favorites --- .../Map/MapContent/MeshMapContent.swift | 51 +++++++++++++++++++ .../Nodes/Helpers/Map/MapSettingsForm.swift | 36 ++++++------- 2 files changed, 69 insertions(+), 18 deletions(-) diff --git a/Meshtastic/Views/Nodes/Helpers/Map/MapContent/MeshMapContent.swift b/Meshtastic/Views/Nodes/Helpers/Map/MapContent/MeshMapContent.swift index 8b3440ac..336b6002 100644 --- a/Meshtastic/Views/Nodes/Helpers/Map/MapContent/MeshMapContent.swift +++ b/Meshtastic/Views/Nodes/Helpers/Map/MapContent/MeshMapContent.swift @@ -52,6 +52,7 @@ struct MeshMapContent: MapContent { ForEach(Array(positions), id: \.id) { position in /// Node color from node.num let nodeColor = UIColor(hex: UInt32(position.nodePosition?.num ?? 0)) + /// Latest Position Anotations Annotation(position.nodePosition?.user?.longName ?? "?", coordinate: position.coordinate) { LazyVStack { ZStack { @@ -87,6 +88,56 @@ struct MeshMapContent: MapContent { selectedPosition = (selectedPosition == position ? nil : position) } } + + /// Node History and Route Lines for favorites + if position.nodePosition?.user?.vip ?? false { + if showRouteLines { + let nodePositions = Array(position.nodePosition!.positions!) as! [PositionEntity] + let routeCoords = nodePositions.compactMap({(pos) -> CLLocationCoordinate2D in + return pos.nodeCoordinate ?? LocationHelper.DefaultLocation + }) + let gradient = LinearGradient( + colors: [Color(nodeColor.lighter().lighter()), Color(nodeColor.lighter()), Color(nodeColor)], + startPoint: .leading, endPoint: .trailing + ) + let dashed = StrokeStyle( + lineWidth: 3, + lineCap: .round, lineJoin: .round, dash: [10, 10] + ) + MapPolyline(coordinates: routeCoords) + .stroke(gradient, style: dashed) + } + if showNodeHistory { + ForEach(Array(position.nodePosition!.positions!) as! [PositionEntity], id: \.self) { (mappin: PositionEntity) in + if mappin.latest == false && mappin.nodePosition?.user?.vip ?? false { + let pf = PositionFlags(rawValue: Int(mappin.nodePosition?.metadata?.positionFlags ?? 771)) + let headingDegrees = Angle.degrees(Double(mappin.heading)) + Annotation("", coordinate: mappin.coordinate) { + LazyVStack { + if pf.contains(.Heading) { + Image(systemName: "location.north.circle") + .resizable() + .scaledToFit() + .foregroundStyle(Color(UIColor(hex: UInt32(mappin.nodePosition?.num ?? 0))).isLight() ? .black : .white) + .background(Color(UIColor(hex: UInt32(mappin.nodePosition?.num ?? 0)))) + .clipShape(Circle()) + .rotationEffect(headingDegrees) + .frame(width: 16, height: 16) + + } else { + Circle() + .fill(Color(UIColor(hex: UInt32(mappin.nodePosition?.num ?? 0)))) + .strokeBorder(Color(UIColor(hex: UInt32(mappin.nodePosition?.num ?? 0))).isLight() ? .black : .white ,lineWidth: 2) + .frame(width: 12, height: 12) + } + } + } + .annotationTitles(.hidden) + .annotationSubtitles(.hidden) + } + } + } + } /// Reduced Precision Map Circles if 11...16 ~= position.precisionBits { let pp = PositionPrecision(rawValue: Int(position.precisionBits)) diff --git a/Meshtastic/Views/Nodes/Helpers/Map/MapSettingsForm.swift b/Meshtastic/Views/Nodes/Helpers/Map/MapSettingsForm.swift index 58405043..17339bf0 100644 --- a/Meshtastic/Views/Nodes/Helpers/Map/MapSettingsForm.swift +++ b/Meshtastic/Views/Nodes/Helpers/Map/MapSettingsForm.swift @@ -40,24 +40,7 @@ struct MapSettingsForm: View { .onChange(of: mapLayer) { newMapLayer in UserDefaults.mapLayer = newMapLayer } - if !meshMap { - Toggle(isOn: $nodeHistory) { - Label("Node History", systemImage: "building.columns.fill") - } - .toggleStyle(SwitchToggleStyle(tint: .accentColor)) - .onTapGesture { - self.nodeHistory.toggle() - UserDefaults.enableMapNodeHistoryPins = self.nodeHistory - } - Toggle(isOn: $routeLines) { - Label("Route Lines", systemImage: "road.lanes") - } - .toggleStyle(SwitchToggleStyle(tint: .accentColor)) - .onTapGesture { - self.routeLines.toggle() - UserDefaults.enableMapRouteLines = self.routeLines - } - } else { + if meshMap { VStack { HStack { Label("Show nodes", systemImage: "lines.measurement.horizontal") @@ -79,6 +62,23 @@ struct MapSettingsForm: View { UserDefaults.meshMapDistance = newMeshMapDistance } } + Toggle(isOn: $nodeHistory) { + Label("Node History", systemImage: "building.columns.fill") + } + .toggleStyle(SwitchToggleStyle(tint: .accentColor)) + .onTapGesture { + self.nodeHistory.toggle() + UserDefaults.enableMapNodeHistoryPins = self.nodeHistory + } + Toggle(isOn: $routeLines) { + Label("Route Lines", systemImage: "road.lanes") + } + + .toggleStyle(SwitchToggleStyle(tint: .accentColor)) + .onTapGesture { + self.routeLines.toggle() + UserDefaults.enableMapRouteLines = self.routeLines + } Toggle(isOn: $convexHull) { Label("Convex Hull", systemImage: "button.angledbottom.horizontal.right") } From b0101ab4ce750ba4b4193a26fc3a6d650a077754 Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Sun, 24 Mar 2024 23:13:35 -0700 Subject: [PATCH 12/34] A little more map cleanup --- .../Views/Nodes/Helpers/Map/MapContent/MeshMapContent.swift | 5 +---- Meshtastic/Views/Nodes/Helpers/Map/MapSettingsForm.swift | 2 +- 2 files changed, 2 insertions(+), 5 deletions(-) diff --git a/Meshtastic/Views/Nodes/Helpers/Map/MapContent/MeshMapContent.swift b/Meshtastic/Views/Nodes/Helpers/Map/MapContent/MeshMapContent.swift index 336b6002..ae1f8a0c 100644 --- a/Meshtastic/Views/Nodes/Helpers/Map/MapContent/MeshMapContent.swift +++ b/Meshtastic/Views/Nodes/Helpers/Map/MapContent/MeshMapContent.swift @@ -28,7 +28,6 @@ struct MeshMapContent: MapContent { // Map Configuration @Binding var selectedPosition: PositionEntity? @Binding var showWaypoints: Bool - //@Binding var editingWaypoint: WaypointEntity? @Binding var selectedWaypoint: WaypointEntity? var delay: Double = 0 @@ -199,8 +198,6 @@ struct MeshMapContent: MapContent { @MapContentBuilder var body: some MapContent { - if positions.count > 0 { - meshMap - } + meshMap } } diff --git a/Meshtastic/Views/Nodes/Helpers/Map/MapSettingsForm.swift b/Meshtastic/Views/Nodes/Helpers/Map/MapSettingsForm.swift index 17339bf0..fa1ce751 100644 --- a/Meshtastic/Views/Nodes/Helpers/Map/MapSettingsForm.swift +++ b/Meshtastic/Views/Nodes/Helpers/Map/MapSettingsForm.swift @@ -119,7 +119,7 @@ Spacer() .padding(.bottom) #endif } - .presentationDetents([.fraction(0.45), .fraction(0.65)]) + .presentationDetents([.fraction(meshMap ? 0.55 : 0.45), .fraction(0.65)]) .presentationDragIndicator(.visible) } From 7eb6659c0c81d79e39374f78e9c55f87419f4a9e Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Mon, 25 Mar 2024 15:21:38 -0700 Subject: [PATCH 13/34] Move fetch requests to mapcontent --- Meshtastic/Enums/AppSettingsEnums.swift | 2 +- .../CoreData/WaypointEntityExtension.swift | 4 +- Meshtastic/Helpers/MeshPackets.swift | 6 +-- .../Map/MapContent/MeshMapContent.swift | 26 +++++++---- .../Map/MapContent/NodeMapContent.swift | 2 - .../Nodes/Helpers/Map/MapSettingsForm.swift | 21 +++------ .../Nodes/Helpers/Map/NodeMapSwiftUI.swift | 21 --------- Meshtastic/Views/Nodes/MeshMap.swift | 46 ++++++++----------- Meshtastic/Views/Nodes/NodeList.swift | 21 ++++++++- .../Settings/Config/PositionConfig.swift | 21 +++++++-- Meshtastic/Views/Settings/Firmware.swift | 2 +- 11 files changed, 88 insertions(+), 84 deletions(-) diff --git a/Meshtastic/Enums/AppSettingsEnums.swift b/Meshtastic/Enums/AppSettingsEnums.swift index fd4fe088..4169444e 100644 --- a/Meshtastic/Enums/AppSettingsEnums.swift +++ b/Meshtastic/Enums/AppSettingsEnums.swift @@ -60,7 +60,7 @@ enum MeshMapDistances: Double, CaseIterable, Identifiable { var id: Double { self.rawValue } var description: String { let distanceFormatter = MKDistanceFormatter() - return "\(distanceFormatter.string(fromDistance: Double(self.rawValue))) away" + return "up to \(distanceFormatter.string(fromDistance: Double(self.rawValue))) away" } } diff --git a/Meshtastic/Extensions/CoreData/WaypointEntityExtension.swift b/Meshtastic/Extensions/CoreData/WaypointEntityExtension.swift index 76fe8f8e..0c3d6854 100644 --- a/Meshtastic/Extensions/CoreData/WaypointEntityExtension.swift +++ b/Meshtastic/Extensions/CoreData/WaypointEntityExtension.swift @@ -13,8 +13,10 @@ extension WaypointEntity { static func allWaypointssFetchRequest() -> NSFetchRequest { let request: NSFetchRequest = WaypointEntity.fetchRequest() - //request.fetchLimit = 100 + request.fetchLimit = 50 //request.fetchBatchSize = 1 + //request.returnsObjectsAsFaults = false + //request.includesSubentities = true request.returnsDistinctResults = true request.sortDescriptors = [NSSortDescriptor(key: "name", ascending: false)] request.predicate = NSPredicate(format: "expire == nil || expire >= %@", Date() as NSDate) diff --git a/Meshtastic/Helpers/MeshPackets.swift b/Meshtastic/Helpers/MeshPackets.swift index 2e1e31cf..0655b89f 100644 --- a/Meshtastic/Helpers/MeshPackets.swift +++ b/Meshtastic/Helpers/MeshPackets.swift @@ -28,9 +28,9 @@ func generateMessageMarkdown (message: String) -> String { let phone = messageWithMarkdown[range] messageWithMarkdown = messageWithMarkdown.replacingOccurrences(of: phone, with: "[\(phone)](tel:\(phone))") } else if match.resultType == .link { - let start = match.range.lowerBound - let stop = match.range.upperBound - if stop > start { + if (match.range.location != NSNotFound) { + let start = match.range.lowerBound + let stop = match.range.upperBound let url = message[start ..< stop] let absoluteUrl = match.url?.absoluteString ?? "" let markdownUrl = "[\(url)](\(absoluteUrl))" diff --git a/Meshtastic/Views/Nodes/Helpers/Map/MapContent/MeshMapContent.swift b/Meshtastic/Views/Nodes/Helpers/Map/MapContent/MeshMapContent.swift index ae1f8a0c..7a96c3d1 100644 --- a/Meshtastic/Views/Nodes/Helpers/Map/MapContent/MeshMapContent.swift +++ b/Meshtastic/Views/Nodes/Helpers/Map/MapContent/MeshMapContent.swift @@ -14,8 +14,7 @@ import MapKit @available(iOS 17.0, macOS 14.0, *) struct MeshMapContent: MapContent { - @State var positions: [PositionEntity] = [] - @State var waypoints: [WaypointEntity] = [] + //@State var waypoints: [WaypointEntity] = [] @State var routes: [RouteEntity] = [] /// Parameters @Binding var showUserLocation: Bool @@ -29,13 +28,19 @@ struct MeshMapContent: MapContent { @Binding var selectedPosition: PositionEntity? @Binding var showWaypoints: Bool @Binding var selectedWaypoint: WaypointEntity? + + @FetchRequest(fetchRequest: PositionEntity.allPositionsFetchRequest(), animation: .easeIn) + var positions: FetchedResults + + @FetchRequest(fetchRequest: WaypointEntity.allWaypointssFetchRequest(), animation: .none) + var waypoints: FetchedResults var delay: Double = 0 @State private var scale: CGFloat = 0.5 @MapContentBuilder var meshMap: some MapContent { - let lineCoords = positions.compactMap({(position) -> CLLocationCoordinate2D in + let lineCoords = Array(positions).compactMap({(position) -> CLLocationCoordinate2D in return position.nodeCoordinate ?? LocationsHandler.DefaultLocation }) /// Convex Hull @@ -148,7 +153,7 @@ struct MeshMapContent: MapContent { } } /// Routes - ForEach(Array(routes), id: \.id) { route in + ForEach(Array(routes)) { route in let routeLocations = Array(route.locations!) as! [LocationEntity] let routeCoords = routeLocations.compactMap({(loc) -> CLLocationCoordinate2D in return loc.locationCoordinate ?? LocationHelper.DefaultLocation @@ -183,13 +188,15 @@ struct MeshMapContent: MapContent { /// Waypoint Annotations if waypoints.count > 0 && showWaypoints { - ForEach(Array(waypoints), id: \.id) { waypoint in + ForEach(Array(waypoints) as! [WaypointEntity], id: \.self) { waypoint in Annotation(waypoint.name ?? "?", coordinate: waypoint.coordinate) { LazyVStack { - CircleText(text: String(UnicodeScalar(Int(waypoint.icon)) ?? "📍"), color: Color.orange, circleSize: 40) - .onTapGesture(perform: { location in - selectedWaypoint = (selectedWaypoint == waypoint ? nil : waypoint) - }) + ZStack { + CircleText(text: String(UnicodeScalar(Int(waypoint.icon)) ?? "📍"), color: Color.orange, circleSize: 40) + .onTapGesture(perform: { location in + selectedWaypoint = (selectedWaypoint == waypoint ? nil : waypoint) + }) + } } } } @@ -199,5 +206,6 @@ struct MeshMapContent: MapContent { @MapContentBuilder var body: some MapContent { meshMap + } } diff --git a/Meshtastic/Views/Nodes/Helpers/Map/MapContent/NodeMapContent.swift b/Meshtastic/Views/Nodes/Helpers/Map/MapContent/NodeMapContent.swift index 2c15e9de..8e8d0b26 100644 --- a/Meshtastic/Views/Nodes/Helpers/Map/MapContent/NodeMapContent.swift +++ b/Meshtastic/Views/Nodes/Helpers/Map/MapContent/NodeMapContent.swift @@ -29,8 +29,6 @@ struct NodeMapContent: MapContent { @State var isShowingAltitude = false @State var isEditingSettings = false @State var selectedPosition: PositionEntity? - @State var showWaypoints = false - @State var selectedWaypoint: WaypointEntity? @State var isMeshMap = false //let region: MKCoordinateRegion diff --git a/Meshtastic/Views/Nodes/Helpers/Map/MapSettingsForm.swift b/Meshtastic/Views/Nodes/Helpers/Map/MapSettingsForm.swift index fa1ce751..6a65c356 100644 --- a/Meshtastic/Views/Nodes/Helpers/Map/MapSettingsForm.swift +++ b/Meshtastic/Views/Nodes/Helpers/Map/MapSettingsForm.swift @@ -41,22 +41,15 @@ struct MapSettingsForm: View { UserDefaults.mapLayer = newMapLayer } if meshMap { - VStack { - HStack { - Label("Show nodes", systemImage: "lines.measurement.horizontal") - Picker("", selection: $meshMapDistance) { - ForEach(MeshMapDistances.allCases) { di in - Text(di.description) - .tag(di.id) - } + HStack { + Label("Show nodes", systemImage: "lines.measurement.horizontal") + Picker("", selection: $meshMapDistance) { + ForEach(MeshMapDistances.allCases) { di in + Text(di.description) + .tag(di.id) } - .pickerStyle(DefaultPickerStyle()) } - .listRowSeparator(.hidden) - Text("You will need to close and re-open the app for this to take effect.") - .font(.callout) - .foregroundColor(.gray) - .listRowSeparator(/*@START_MENU_TOKEN@*/.visible/*@END_MENU_TOKEN@*/) + .pickerStyle(DefaultPickerStyle()) } .onChange(of: meshMapDistance) { newMeshMapDistance in UserDefaults.meshMapDistance = newMeshMapDistance diff --git a/Meshtastic/Views/Nodes/Helpers/Map/NodeMapSwiftUI.swift b/Meshtastic/Views/Nodes/Helpers/Map/NodeMapSwiftUI.swift index 3f2fb0fb..282a5a8c 100644 --- a/Meshtastic/Views/Nodes/Helpers/Map/NodeMapSwiftUI.swift +++ b/Meshtastic/Views/Nodes/Helpers/Map/NodeMapSwiftUI.swift @@ -36,8 +36,6 @@ struct NodeMapSwiftUI: View { @State var isShowingAltitude = false @State var isEditingSettings = false @State var selectedPosition: PositionEntity? - @State var showWaypoints = false - @State var selectedWaypoint: WaypointEntity? @State var isMeshMap = false @State private var mapRegion = MKCoordinateRegion.init() @@ -88,10 +86,6 @@ struct NodeMapSwiftUI: View { .padding(.horizontal, 20) } } - .sheet(item: $selectedWaypoint) { selection in - WaypointForm(waypoint: selection) - .padding() - } .sheet(isPresented: $isEditingSettings) { MapSettingsForm(nodeHistory: $showNodeHistory, routeLines: $showRouteLines, convexHull: $showConvexHull, traffic: $showTraffic, pointsOfInterest: $showPointsOfInterest, mapLayer: $selectedMapLayer, meshMapDistance: $meshMapDistance, meshMap: $isMeshMap) .onChange(of: (selectedMapLayer)) { newMapLayer in @@ -162,21 +156,6 @@ struct NodeMapSwiftUI: View { .tint(Color(UIColor.secondarySystemBackground)) .foregroundColor(.accentColor) .buttonStyle(.borderedProminent) - /// Show / Hide Waypoints Button - if waypoints.count > 0 { - - Button(action: { - withAnimation { - showWaypoints = !showWaypoints - } - }) { - Image(systemName: showWaypoints ? "signpost.right.and.left.fill" : "signpost.right.and.left") - .padding(.vertical, 5) - } - .tint(Color(UIColor.secondarySystemBackground)) - .foregroundColor(.accentColor) - .buttonStyle(.borderedProminent) - } /// Look Around Button if self.scene != nil { Button(action: { diff --git a/Meshtastic/Views/Nodes/MeshMap.swift b/Meshtastic/Views/Nodes/MeshMap.swift index 780ebde2..b992635f 100644 --- a/Meshtastic/Views/Nodes/MeshMap.swift +++ b/Meshtastic/Views/Nodes/MeshMap.swift @@ -37,17 +37,12 @@ struct MeshMap: View { @State var position = MapCameraPosition.automatic @State var isEditingSettings = false @State var selectedPosition: PositionEntity? - @State var showWaypoints = false + @State var showWaypoints = true @State var editingWaypoint: WaypointEntity? @State var selectedWaypoint: WaypointEntity? @State var newWaypointCoord: CLLocationCoordinate2D? @State var isMeshMap = true - - @FetchRequest(fetchRequest: PositionEntity.allPositionsFetchRequest(), animation: .none) - var positions: FetchedResults - - @FetchRequest(fetchRequest: WaypointEntity.allWaypointssFetchRequest(), animation: .none) - var waypoints: FetchedResults + @FetchRequest(sortDescriptors: [NSSortDescriptor(key: "name", ascending: true)], predicate: NSPredicate(format: "enabled == true", ""), animation: .none) @@ -59,7 +54,7 @@ struct MeshMap: View { ZStack { MapReader { reader in Map(position: $position, bounds: MapCameraBounds(minimumDistance: 1, maximumDistance: .infinity), scope: mapScope) { - MeshMapContent(positions: Array(positions), waypoints: Array(waypoints), routes: Array(routes), showUserLocation: $showUserLocation, showNodeHistory: $showNodeHistory, showRouteLines: $showRouteLines, showConvexHull: $showConvexHull, showTraffic: $showTraffic, showPointsOfInterest: $showPointsOfInterest, selectedMapLayer: $selectedMapLayer, selectedPosition: $selectedPosition, showWaypoints: $showWaypoints, selectedWaypoint: $selectedWaypoint) + MeshMapContent(routes: Array(routes), showUserLocation: $showUserLocation, showNodeHistory: $showNodeHistory, showRouteLines: $showRouteLines, showConvexHull: $showConvexHull, showTraffic: $showTraffic, showPointsOfInterest: $showPointsOfInterest, selectedMapLayer: $selectedMapLayer, selectedPosition: $selectedPosition, showWaypoints: $showWaypoints, selectedWaypoint: $selectedWaypoint) } .mapScope(mapScope) @@ -142,12 +137,12 @@ struct MeshMap: View { print("Waypoint id not found") return } - guard let waypoint = waypoints.first(where: { $0.id == Int64(waypointId) }) else { - print("Waypoint not found") - return - } +// guard let waypoint = waypoints.first(where: { $0.id == Int64(waypointId) }) else { +// print("Waypoint not found") +// return +// } showWaypoints = true - position = .camera(MapCamera(centerCoordinate: waypoint.coordinate, distance: 1000, heading: 0, pitch: 60)) + //position = .camera(MapCamera(centerCoordinate: waypoint.coordinate, distance: 1000, heading: 0, pitch: 60)) } } .onChange(of: (selectedMapLayer)) { newMapLayer in @@ -179,26 +174,25 @@ struct MeshMap: View { .foregroundColor(.accentColor) .buttonStyle(.borderedProminent) /// Show / Hide Waypoints Button - if waypoints.count > 0 { - - Button(action: { - withAnimation { - showWaypoints = !showWaypoints - } - }) { - Image(systemName: showWaypoints ? "signpost.right.and.left.fill" : "signpost.right.and.left") - .padding(.vertical, 5) + + Button(action: { + withAnimation { + showWaypoints = !showWaypoints } - .tint(Color(UIColor.secondarySystemBackground)) - .foregroundColor(.accentColor) - .buttonStyle(.borderedProminent) + }) { + Image(systemName: showWaypoints ? "signpost.right.and.left.fill" : "signpost.right.and.left") + .padding(.vertical, 5) } + .tint(Color(UIColor.secondarySystemBackground)) + .foregroundColor(.accentColor) + .buttonStyle(.borderedProminent) + } .controlSize(.regular) .padding(5) } } - .navigationTitle("\(positions.count) Nodes") + .navigationTitle("Mesh Map") .navigationBarItems(leading: MeshtasticLogo(), trailing: ZStack { ConnectedDevice(bluetoothOn: bleManager.isSwitchedOn, deviceConnected: bleManager.connectedPeripheral != nil, name: (bleManager.connectedPeripheral != nil) ? bleManager.connectedPeripheral.shortName : "?") }) diff --git a/Meshtastic/Views/Nodes/NodeList.swift b/Meshtastic/Views/Nodes/NodeList.swift index c2532ad7..902d7df5 100644 --- a/Meshtastic/Views/Nodes/NodeList.swift +++ b/Meshtastic/Views/Nodes/NodeList.swift @@ -159,6 +159,25 @@ struct NodeList: View { Text("Any missed messages will be delivered again.") } } + .safeAreaInset(edge: .bottom, alignment: .trailing) { + HStack { + Button(action: { + withAnimation { + //isEditingSettings = !isEditingSettings + } + }) { + Image(systemName: true ? "line.3.horizontal.decrease.circle" : "line.3.horizontal.decrease.circle.fill") + .padding(.vertical, 5) + } + .tint(Color(UIColor.secondarySystemBackground)) + .foregroundColor(.accentColor) + .buttonStyle(.borderedProminent) + + } + .controlSize(.regular) + .padding(5) + } + .padding(.bottom, 5) .searchable(text: $searchState.searchText, placement: nodes.count > 10 ? .navigationBarDrawer(displayMode: .always) : .automatic, prompt: "Find a node") .disableAutocorrection(true) .scrollDismissesKeyboard(.immediately) @@ -220,7 +239,7 @@ struct NodeList: View { name: (bleManager.connectedPeripheral != nil) ? bleManager.connectedPeripheral.shortName : "?", phoneOnly: true) }) } - .padding(.bottom, 5) + } else { if #available (iOS 17, *) { ContentUnavailableView("select.node", systemImage: "flipphone") diff --git a/Meshtastic/Views/Settings/Config/PositionConfig.swift b/Meshtastic/Views/Settings/Config/PositionConfig.swift index 9f96ef8b..b18ff5e6 100644 --- a/Meshtastic/Views/Settings/Config/PositionConfig.swift +++ b/Meshtastic/Views/Settings/Config/PositionConfig.swift @@ -72,6 +72,10 @@ struct PositionConfig: View { /// walking speeds are likely to be error prone like the compass @State var includeHeading = false + /// Minimum Version for fixed postion admin messages + @State var minimumVersion = "2.3.3" + @State private var supportedVersion = true + var body: some View { VStack { Form { @@ -159,6 +163,15 @@ struct PositionConfig: View { Text("If enabled your current phone location will be sent to the device and will broadcast over the mesh on the position interval. Fixed position will always use the most recent position the device has.") } .toggleStyle(SwitchToggleStyle(tint: .accentColor)) + + } + .onChange(of: fixedPosition) { newFixed in + if node != nil && node!.positionConfig != nil { + if newFixed != node!.positionConfig!.fixedPosition { hasChanges = true } + } + if supportedVersion && hasChanges && !newFixed { + // Send Admin message to remove the fixed position + } } } } @@ -316,6 +329,9 @@ struct PositionConfig: View { self.bleManager.context = context } setPositionValues() + supportedVersion = bleManager.connectedVersion == "0.0.0" || self.minimumVersion.compare(bleManager.connectedVersion, options: .numeric) == .orderedAscending || minimumVersion.compare(bleManager.connectedVersion, options: .numeric) == .orderedSame + + // Need to request a PositionConfig from the remote node before allowing changes if bleManager.connectedPeripheral != nil && node?.positionConfig == nil { print("empty position config") @@ -355,11 +371,6 @@ struct PositionConfig: View { if newSmartPositionEnabled != node!.positionConfig!.smartPositionEnabled { hasChanges = true } } } - .onChange(of: fixedPosition) { newFixed in - if node != nil && node!.positionConfig != nil { - if newFixed != node!.positionConfig!.fixedPosition { hasChanges = true } - } - } .onChange(of: positionBroadcastSeconds) { newPositionBroadcastSeconds in if node != nil && node!.positionConfig != nil { if newPositionBroadcastSeconds != node!.positionConfig!.positionBroadcastSeconds { hasChanges = true } diff --git a/Meshtastic/Views/Settings/Firmware.swift b/Meshtastic/Views/Settings/Firmware.swift index 98b29957..3558da1f 100644 --- a/Meshtastic/Views/Settings/Firmware.swift +++ b/Meshtastic/Views/Settings/Firmware.swift @@ -12,7 +12,7 @@ struct Firmware: View { @Environment(\.managedObjectContext) var context @EnvironmentObject var bleManager: BLEManager var node: NodeInfoEntity? - @State var minimumVersion = "2.3.0" + @State var minimumVersion = "2.3.2" @State var version = "" @State private var currentDevice: DeviceHardware? @State private var latestStable: FirmwareRelease? From 6577eaaa9fc0c651b522fd03475901719ba526e7 Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Mon, 25 Mar 2024 15:49:10 -0700 Subject: [PATCH 14/34] Turn waypoints off by default --- Meshtastic/Views/Nodes/MeshMap.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Meshtastic/Views/Nodes/MeshMap.swift b/Meshtastic/Views/Nodes/MeshMap.swift index b992635f..8dee5f86 100644 --- a/Meshtastic/Views/Nodes/MeshMap.swift +++ b/Meshtastic/Views/Nodes/MeshMap.swift @@ -37,7 +37,7 @@ struct MeshMap: View { @State var position = MapCameraPosition.automatic @State var isEditingSettings = false @State var selectedPosition: PositionEntity? - @State var showWaypoints = true + @State var showWaypoints = false @State var editingWaypoint: WaypointEntity? @State var selectedWaypoint: WaypointEntity? @State var newWaypointCoord: CLLocationCoordinate2D? From 594ece35a504d75e9a27ee8c529fd8e57aa98e41 Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Mon, 25 Mar 2024 17:19:03 -0700 Subject: [PATCH 15/34] Dont crash when you get emoji --- Meshtastic/Helpers/MeshPackets.swift | 35 ++++++++++++++-------------- 1 file changed, 18 insertions(+), 17 deletions(-) diff --git a/Meshtastic/Helpers/MeshPackets.swift b/Meshtastic/Helpers/MeshPackets.swift index 0655b89f..f05bb3cd 100644 --- a/Meshtastic/Helpers/MeshPackets.swift +++ b/Meshtastic/Helpers/MeshPackets.swift @@ -13,22 +13,22 @@ import ActivityKit #endif func generateMessageMarkdown (message: String) -> String { - let types: NSTextCheckingResult.CheckingType = [.address, .link, .phoneNumber] - let detector = try! NSDataDetector(types: types.rawValue) - let matches = detector.matches(in: message, options: [], range: NSRange(location: 0, length: message.utf8.count)) - var messageWithMarkdown = message - if matches.count > 0 { - for match in matches { - guard let range = Range(match.range, in: message) else { continue } - if match.resultType == .address { - let address = message[range] - let urlEncodedAddress = address.addingPercentEncoding(withAllowedCharacters: .alphanumerics) - messageWithMarkdown = messageWithMarkdown.replacingOccurrences(of: address, with: "[\(address)](http://maps.apple.com/?address=\(urlEncodedAddress ?? ""))") - } else if match.resultType == .phoneNumber { - let phone = messageWithMarkdown[range] - messageWithMarkdown = messageWithMarkdown.replacingOccurrences(of: phone, with: "[\(phone)](tel:\(phone))") - } else if match.resultType == .link { - if (match.range.location != NSNotFound) { + if !message.isEmoji() { + let types: NSTextCheckingResult.CheckingType = [.address, .link, .phoneNumber] + let detector = try! NSDataDetector(types: types.rawValue) + let matches = detector.matches(in: message, options: [], range: NSRange(location: 0, length: message.utf16.count)) + var messageWithMarkdown = message + if matches.count > 0 { + for match in matches { + guard let range = Range(match.range, in: message) else { continue } + if match.resultType == .address { + let address = message[range] + let urlEncodedAddress = address.addingPercentEncoding(withAllowedCharacters: .alphanumerics) + messageWithMarkdown = messageWithMarkdown.replacingOccurrences(of: address, with: "[\(address)](http://maps.apple.com/?address=\(urlEncodedAddress ?? ""))") + } else if match.resultType == .phoneNumber { + let phone = messageWithMarkdown[range] + messageWithMarkdown = messageWithMarkdown.replacingOccurrences(of: phone, with: "[\(phone)](tel:\(phone))") + } else if match.resultType == .link { let start = match.range.lowerBound let stop = match.range.upperBound let url = message[start ..< stop] @@ -38,8 +38,9 @@ func generateMessageMarkdown (message: String) -> String { } } } + return messageWithMarkdown } - return messageWithMarkdown + return message } func localConfig (config: Config, context: NSManagedObjectContext, nodeNum: Int64, nodeLongName: String) { From 80103ebda28b711a408896ca9ca94be452b0b31e Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Mon, 25 Mar 2024 18:43:03 -0700 Subject: [PATCH 16/34] Revert user default updates, move everything to the map settings form --- Meshtastic/Extensions/UserDefaults.swift | 439 ++++++++++++++---- .../Map/MapContent/MeshMapContent.swift | 8 +- .../Nodes/Helpers/Map/MapSettingsForm.swift | 16 +- .../Nodes/Helpers/Map/NodeMapSwiftUI.swift | 6 +- Meshtastic/Views/Nodes/MeshMap.swift | 27 +- 5 files changed, 363 insertions(+), 133 deletions(-) diff --git a/Meshtastic/Extensions/UserDefaults.swift b/Meshtastic/Extensions/UserDefaults.swift index b497495d..ccb8a7a3 100644 --- a/Meshtastic/Extensions/UserDefaults.swift +++ b/Meshtastic/Extensions/UserDefaults.swift @@ -4,29 +4,15 @@ // // Copyright(c) Garth Vander Houwen 4/24/23. // +// +// UserDefaults.swift +// Meshtastic +// +// Copyright(c) Garth Vander Houwen 4/24/23. +// import Foundation -@propertyWrapper -struct UserDefault { - let key: UserDefaults.Keys - let defaultValue: T - - init(_ key: UserDefaults.Keys, defaultValue: T) { - self.key = key - self.defaultValue = defaultValue - } - - var wrappedValue: T { - get { - UserDefaults.standard.object(forKey: key.rawValue) as? T ?? defaultValue - } - set { - UserDefaults.standard.set(newValue, forKey: key.rawValue) - } - } -} - extension UserDefaults { enum Keys: String, CaseIterable { case preferredPeripheralId @@ -35,20 +21,15 @@ extension UserDefaults { case provideLocationInterval case mapLayer case meshMapDistance + case enableMapWaypoints case meshMapRecentering case meshMapShowNodeHistory case meshMapShowRouteLines case enableMapConvexHull - case enableMapRecentering - case enableMapNodeHistoryPins - case enableMapRouteLines case enableMapTraffic case enableMapPointsOfInterest case enableOfflineMaps - case enableOfflineMapsMBTiles case mapTileServer - case enableOverlayServer - case mapOverlayServer case mapTilesAboveLabels case mapUseLegacy case enableDetectionNotifications @@ -61,76 +42,340 @@ extension UserDefaults { func reset() { Keys.allCases.forEach { removeObject(forKey: $0.rawValue) } } - - @UserDefault(.preferredPeripheralId, defaultValue: "") - static var preferredPeripheralId: String - - @UserDefault(.preferredPeripheralNum, defaultValue: 0) - static var preferredPeripheralNum: Int - - @UserDefault(.provideLocation, defaultValue: false) - static var provideLocation: Bool - - @UserDefault(.provideLocationInterval, defaultValue: 0) - static var provideLocationInterval: Int - - @UserDefault(.mapLayer, defaultValue: .standard) - static var mapLayer: MapLayer - - @UserDefault(.meshMapDistance, defaultValue: 800000) - static var meshMapDistance: Double + static var preferredPeripheralId: String { + get { + UserDefaults.standard.string(forKey: "preferredPeripheralId") ?? "" + } + set { + UserDefaults.standard.set(newValue, forKey: "preferredPeripheralId") + } + } + static var preferredPeripheralNum: Int { + get { + UserDefaults.standard.integer(forKey: "preferredPeripheralNum") + } + set { + UserDefaults.standard.set(newValue, forKey: "preferredPeripheralNum") + } + } + static var provideLocation: Bool { + get { + UserDefaults.standard.bool(forKey: "provideLocation") + } set { + UserDefaults.standard.set(newValue, forKey: "provideLocation") + } + } + static var provideLocationInterval: Int { + get { + UserDefaults.standard.integer(forKey: "provideLocationInterval") + } + set { + UserDefaults.standard.set(newValue, forKey: "provideLocationInterval") + } + } + static var mapLayer: MapLayer { + get { + MapLayer(rawValue: UserDefaults.standard.string(forKey: "mapLayer") ?? MapLayer.standard.rawValue) ?? MapLayer.standard + } + set { + UserDefaults.standard.set(newValue.rawValue, forKey: "mapLayer") + } + } + static var meshMapDistance: Double { + get { + UserDefaults.standard.double(forKey: "meshMapDistance") + } + set { + UserDefaults.standard.set(newValue, forKey: "meshMapDistance") + } + } + static var enableMapWaypoints: Bool { + get { + UserDefaults.standard.bool(forKey: "enableMapWaypoints") + } + set { + UserDefaults.standard.set(newValue, forKey: "enableMapWaypoints") + } + } + static var enableMapRecentering: Bool { + get { + UserDefaults.standard.bool(forKey: "meshMapRecentering") + } + set { + UserDefaults.standard.set(newValue, forKey: "meshMapRecentering") + } + } + static var enableMapNodeHistoryPins: Bool { + get { + UserDefaults.standard.bool(forKey: "meshMapShowNodeHistory") + } + set { + UserDefaults.standard.set(newValue, forKey: "meshMapShowNodeHistory") + } + } + static var enableMapRouteLines: Bool { + get { + UserDefaults.standard.bool(forKey: "meshMapShowRouteLines") + } + set { + UserDefaults.standard.set(newValue, forKey: "meshMapShowRouteLines") + } + } + static var enableMapConvexHull: Bool { + get { + UserDefaults.standard.bool(forKey: "enableMapConvexHull") + } + set { + UserDefaults.standard.set(newValue, forKey: "enableMapConvexHull") + } + } + static var enableMapTraffic: Bool { + get { + UserDefaults.standard.bool(forKey: "enableMapTraffic") + } + set { + UserDefaults.standard.set(newValue, forKey: "enableMapTraffic") + } + } + static var enableMapPointsOfInterest: Bool { + get { + UserDefaults.standard.bool(forKey: "enableMapPointsOfInterest") + } + set { + UserDefaults.standard.set(newValue, forKey: "enableMapPointsOfInterest") + } + } + static var enableOfflineMaps: Bool { + get { + UserDefaults.standard.bool(forKey: "enableOfflineMaps") + } + set { + UserDefaults.standard.set(newValue, forKey: "enableOfflineMaps") + } + } + static var enableOfflineMapsMBTiles: Bool { + get { + UserDefaults.standard.bool(forKey: "enableOfflineMapsMBTiles") + } + set { + UserDefaults.standard.set(newValue, forKey: "enableOfflineMapsMBTiles") + } + } + static var mapTileServer: MapTileServer { + get { + MapTileServer(rawValue: UserDefaults.standard.string(forKey: "mapTileServer") ?? MapTileServer.openStreetMap.rawValue) ?? MapTileServer.openStreetMap + } + set { + UserDefaults.standard.set(newValue.rawValue, forKey: "mapTileServer") + } + } + static var enableOverlayServer: Bool { + get { + UserDefaults.standard.bool(forKey: "enableOverlayServer") + } + set { + UserDefaults.standard.set(newValue, forKey: "enableOverlayServer") + } + } + static var mapOverlayServer: MapOverlayServer { + get { + MapOverlayServer(rawValue: UserDefaults.standard.string(forKey: "mapOverlayServer") ?? MapOverlayServer.baseReReflectivityCurrent.rawValue) ?? MapOverlayServer.baseReReflectivityCurrent + } + set { + UserDefaults.standard.set(newValue.rawValue, forKey: "mapOverlayServer") + } + } + static var mapTilesAboveLabels: Bool { + get { + UserDefaults.standard.bool(forKey: "mapTilesAboveLabels") + } + set { + UserDefaults.standard.set(newValue, forKey: "mapTilesAboveLabels") + } + } - @UserDefault(.enableMapRecentering, defaultValue: false) - static var enableMapRecentering: Bool - - @UserDefault(.enableMapNodeHistoryPins, defaultValue: false) - static var enableMapNodeHistoryPins: Bool - - @UserDefault(.enableMapRouteLines, defaultValue: false) - static var enableMapRouteLines: Bool - - @UserDefault(.enableMapConvexHull, defaultValue: false) - static var enableMapConvexHull: Bool - - @UserDefault(.enableMapTraffic, defaultValue: false) - static var enableMapTraffic: Bool - - @UserDefault(.enableMapPointsOfInterest, defaultValue: false) - static var enableMapPointsOfInterest: Bool - - @UserDefault(.enableOfflineMaps, defaultValue: false) - static var enableOfflineMaps: Bool - - @UserDefault(.enableOfflineMapsMBTiles, defaultValue: false) - static var enableOfflineMapsMBTiles: Bool - - @UserDefault(.mapTileServer, defaultValue: .openStreetMap) - static var mapTileServer: MapTileServer - - @UserDefault(.enableOverlayServer, defaultValue: false) - static var enableOverlayServer: Bool - - @UserDefault(.mapOverlayServer, defaultValue: .baseReReflectivityCurrent) - static var mapOverlayServer: MapOverlayServer - - @UserDefault(.mapTilesAboveLabels, defaultValue: false) - static var mapTilesAboveLabels: Bool - - @UserDefault(.mapUseLegacy, defaultValue: false) - static var mapUseLegacy: Bool - - @UserDefault(.enableDetectionNotifications, defaultValue: false) - static var enableDetectionNotifications: Bool - - @UserDefault(.detectionSensorRole, defaultValue: .sensor) - static var detectionSensorRole: DetectionSensorRole - - @UserDefault(.enableSmartPosition, defaultValue: false) - static var enableSmartPosition: Bool - - @UserDefault(.modemPreset, defaultValue: 0) - static var modemPreset: Int - - @UserDefault(.firmwareVersion, defaultValue: "0.0.0") - static var firmwareVersion: String + static var mapUseLegacy: Bool { + get { + UserDefaults.standard.bool(forKey: "mapUseLegacy") + } + set { + UserDefaults.standard.set(newValue, forKey: "mapUseLegacy") + } + } + + static var enableDetectionNotifications: Bool { + get { + UserDefaults.standard.bool(forKey: "enableDetectionNotifications") + } + set { + UserDefaults.standard.set(newValue, forKey: "enableDetectionNotifications") + } + } + + static var detectionSensorRole: DetectionSensorRole { + get { + DetectionSensorRole(rawValue: UserDefaults.standard.string(forKey: "detectionSensorRole") ?? DetectionSensorRole.sensor.rawValue) ?? DetectionSensorRole.sensor + } + set { + UserDefaults.standard.set(newValue.rawValue, forKey: "detectionSensorRole") + } + } + static var enableSmartPosition: Bool { + get { + UserDefaults.standard.bool(forKey: "enableSmartPosition") + } + set { + UserDefaults.standard.set(newValue, forKey: "enableSmartPosition") + } + } + static var modemPreset: Int { + get { + UserDefaults.standard.integer(forKey: "modemPreset") + } + set { + UserDefaults.standard.set(newValue, forKey: "modemPreset") + } + } + static var firmwareVersion: String { + get { + UserDefaults.standard.string(forKey: "firmwareVersion") ?? "0.0.0" + } + set { + UserDefaults.standard.set(newValue, forKey: "firmwareVersion") + } + } } + +//import Foundation +// +//@propertyWrapper +//struct UserDefault { +// let key: UserDefaults.Keys +// let defaultValue: T +// +// init(_ key: UserDefaults.Keys, defaultValue: T) { +// self.key = key +// self.defaultValue = defaultValue +// } +// +// var wrappedValue: T { +// get { +// UserDefaults.standard.object(forKey: key.rawValue) as? T ?? defaultValue +// } +// set { +// UserDefaults.standard.set(newValue, forKey: key.rawValue) +// } +// } +//} +// +//extension UserDefaults { +// enum Keys: String, CaseIterable { +// case preferredPeripheralId +// case preferredPeripheralNum +// case provideLocation +// case provideLocationInterval +// case mapLayer +// case meshMapDistance +// case enableMapWaypoints +// case meshMapRecentering +// case meshMapShowNodeHistory +// case meshMapShowRouteLines +// case enableMapConvexHull +// case enableMapRecentering +// case enableMapNodeHistoryPins +// case enableMapRouteLines +// case enableMapTraffic +// case enableMapPointsOfInterest +// case enableOfflineMaps +// case enableOfflineMapsMBTiles +// case mapTileServer +// case enableOverlayServer +// case mapOverlayServer +// case mapTilesAboveLabels +// case mapUseLegacy +// case enableDetectionNotifications +// case detectionSensorRole +// case enableSmartPosition +// case modemPreset +// case firmwareVersion +// } +// +// func reset() { +// Keys.allCases.forEach { removeObject(forKey: $0.rawValue) } +// } +// +// @UserDefault(.preferredPeripheralId, defaultValue: "") +// static var preferredPeripheralId: String +// +// @UserDefault(.preferredPeripheralNum, defaultValue: 0) +// static var preferredPeripheralNum: Int +// +// @UserDefault(.provideLocation, defaultValue: false) +// static var provideLocation: Bool +// +// @UserDefault(.provideLocationInterval, defaultValue: 0) +// static var provideLocationInterval: Int +// +// @UserDefault(.mapLayer, defaultValue: .standard) +// static var mapLayer: MapLayer +// +// @UserDefault(.meshMapDistance, defaultValue: 800000) +// static var meshMapDistance: Double +// +// @UserDefault(.enableMapWaypoints, defaultValue: false) +// static var enableMapWaypoints: Bool +// +// @UserDefault(.enableMapRecentering, defaultValue: false) +// static var enableMapRecentering: Bool +// +// @UserDefault(.enableMapNodeHistoryPins, defaultValue: false) +// static var enableMapNodeHistoryPins: Bool +// +// @UserDefault(.enableMapRouteLines, defaultValue: false) +// static var enableMapRouteLines: Bool +// +// @UserDefault(.enableMapConvexHull, defaultValue: false) +// static var enableMapConvexHull: Bool +// +// @UserDefault(.enableMapTraffic, defaultValue: false) +// static var enableMapTraffic: Bool +// +// @UserDefault(.enableMapPointsOfInterest, defaultValue: false) +// static var enableMapPointsOfInterest: Bool +// +// @UserDefault(.enableOfflineMaps, defaultValue: false) +// static var enableOfflineMaps: Bool +// +// @UserDefault(.enableOfflineMapsMBTiles, defaultValue: false) +// static var enableOfflineMapsMBTiles: Bool +// +// @UserDefault(.mapTileServer, defaultValue: .openStreetMap) +// static var mapTileServer: MapTileServer +// +// @UserDefault(.enableOverlayServer, defaultValue: false) +// static var enableOverlayServer: Bool +// +// @UserDefault(.mapOverlayServer, defaultValue: .baseReReflectivityCurrent) +// static var mapOverlayServer: MapOverlayServer +// +// @UserDefault(.mapTilesAboveLabels, defaultValue: false) +// static var mapTilesAboveLabels: Bool +// +// @UserDefault(.mapUseLegacy, defaultValue: false) +// static var mapUseLegacy: Bool +// +// @UserDefault(.enableDetectionNotifications, defaultValue: false) +// static var enableDetectionNotifications: Bool +// +// @UserDefault(.detectionSensorRole, defaultValue: .sensor) +// static var detectionSensorRole: DetectionSensorRole +// +// @UserDefault(.enableSmartPosition, defaultValue: false) +// static var enableSmartPosition: Bool +// +// @UserDefault(.modemPreset, defaultValue: 0) +// static var modemPreset: Int +// +// @UserDefault(.firmwareVersion, defaultValue: "0.0.0") +// static var firmwareVersion: String +//} diff --git a/Meshtastic/Views/Nodes/Helpers/Map/MapContent/MeshMapContent.swift b/Meshtastic/Views/Nodes/Helpers/Map/MapContent/MeshMapContent.swift index 7a96c3d1..e64febb5 100644 --- a/Meshtastic/Views/Nodes/Helpers/Map/MapContent/MeshMapContent.swift +++ b/Meshtastic/Views/Nodes/Helpers/Map/MapContent/MeshMapContent.swift @@ -18,15 +18,15 @@ struct MeshMapContent: MapContent { @State var routes: [RouteEntity] = [] /// Parameters @Binding var showUserLocation: Bool - @Binding var showNodeHistory: Bool - @Binding var showRouteLines: Bool - @Binding var showConvexHull: Bool + @AppStorage("meshMapShowNodeHistory") private var showNodeHistory = false + @AppStorage("meshMapShowRouteLines") private var showRouteLines = false + @AppStorage("enableMapConvexHull") private var showConvexHull = false @Binding var showTraffic: Bool @Binding var showPointsOfInterest: Bool @Binding var selectedMapLayer: MapLayer // Map Configuration @Binding var selectedPosition: PositionEntity? - @Binding var showWaypoints: Bool + @AppStorage("enableMapWaypoints") private var showWaypoints = false @Binding var selectedWaypoint: WaypointEntity? @FetchRequest(fetchRequest: PositionEntity.allPositionsFetchRequest(), animation: .easeIn) diff --git a/Meshtastic/Views/Nodes/Helpers/Map/MapSettingsForm.swift b/Meshtastic/Views/Nodes/Helpers/Map/MapSettingsForm.swift index 6a65c356..80d6a61f 100644 --- a/Meshtastic/Views/Nodes/Helpers/Map/MapSettingsForm.swift +++ b/Meshtastic/Views/Nodes/Helpers/Map/MapSettingsForm.swift @@ -13,13 +13,14 @@ import MapKit @available(iOS 17.0, macOS 14.0, *) struct MapSettingsForm: View { @Environment(\.dismiss) private var dismiss - @Binding var nodeHistory: Bool - @Binding var routeLines: Bool - @Binding var convexHull: Bool + @AppStorage("meshMapShowNodeHistory") private var nodeHistory = false + @AppStorage("meshMapShowRouteLines") private var routeLines = false + @AppStorage("enableMapConvexHull") private var convexHull = false + @AppStorage("enableMapWaypoints") private var waypoints = false @Binding var traffic: Bool @Binding var pointsOfInterest: Bool @Binding var mapLayer: MapLayer - @Binding var meshMapDistance: Double + @AppStorage("meshMapDistance") private var meshMapDistance: Double = 800000 @Binding var meshMap: Bool var body: some View { @@ -55,6 +56,13 @@ struct MapSettingsForm: View { UserDefaults.meshMapDistance = newMeshMapDistance } } + Toggle(isOn: $waypoints) { + Label("Show Waypoints ", systemImage: "signpost.right.and.left") + } + .toggleStyle(SwitchToggleStyle(tint: .accentColor)) + .onTapGesture { + UserDefaults.enableMapWaypoints = !waypoints + } Toggle(isOn: $nodeHistory) { Label("Node History", systemImage: "building.columns.fill") } diff --git a/Meshtastic/Views/Nodes/Helpers/Map/NodeMapSwiftUI.swift b/Meshtastic/Views/Nodes/Helpers/Map/NodeMapSwiftUI.swift index 282a5a8c..bd512e94 100644 --- a/Meshtastic/Views/Nodes/Helpers/Map/NodeMapSwiftUI.swift +++ b/Meshtastic/Views/Nodes/Helpers/Map/NodeMapSwiftUI.swift @@ -20,10 +20,6 @@ struct NodeMapSwiftUI: View { @State var showUserLocation: Bool = false @State var positions: [PositionEntity] = [] /// Map State User Defaults - @AppStorage("meshMapDistance") private var meshMapDistance: Double = 800000 - @AppStorage("meshMapShowNodeHistory") private var showNodeHistory = false - @AppStorage("meshMapShowRouteLines") private var showRouteLines = false - @AppStorage("enableMapConvexHull") private var showConvexHull = false @AppStorage("enableMapTraffic") private var showTraffic: Bool = false @AppStorage("enableMapPointsOfInterest") private var showPointsOfInterest: Bool = false @AppStorage("mapLayer") private var selectedMapLayer: MapLayer = .hybrid @@ -87,7 +83,7 @@ struct NodeMapSwiftUI: View { } } .sheet(isPresented: $isEditingSettings) { - MapSettingsForm(nodeHistory: $showNodeHistory, routeLines: $showRouteLines, convexHull: $showConvexHull, traffic: $showTraffic, pointsOfInterest: $showPointsOfInterest, mapLayer: $selectedMapLayer, meshMapDistance: $meshMapDistance, meshMap: $isMeshMap) + MapSettingsForm(traffic: $showTraffic, pointsOfInterest: $showPointsOfInterest, mapLayer: $selectedMapLayer, meshMap: $isMeshMap) .onChange(of: (selectedMapLayer)) { newMapLayer in switch selectedMapLayer { case .standard: diff --git a/Meshtastic/Views/Nodes/MeshMap.swift b/Meshtastic/Views/Nodes/MeshMap.swift index 8dee5f86..0787ea55 100644 --- a/Meshtastic/Views/Nodes/MeshMap.swift +++ b/Meshtastic/Views/Nodes/MeshMap.swift @@ -24,10 +24,6 @@ struct MeshMap: View { /// Parameters @State var showUserLocation: Bool = true /// Map State User Defaults - @AppStorage("meshMapDistance") private var meshMapDistance: Double = 800000 - @AppStorage("meshMapShowNodeHistory") private var showNodeHistory = false - @AppStorage("meshMapShowRouteLines") private var showRouteLines = false - @AppStorage("enableMapConvexHull") private var showConvexHull = false @AppStorage("enableMapTraffic") private var showTraffic: Bool = false @AppStorage("enableMapPointsOfInterest") private var showPointsOfInterest: Bool = false @AppStorage("mapLayer") private var selectedMapLayer: MapLayer = .standard @@ -37,7 +33,6 @@ struct MeshMap: View { @State var position = MapCameraPosition.automatic @State var isEditingSettings = false @State var selectedPosition: PositionEntity? - @State var showWaypoints = false @State var editingWaypoint: WaypointEntity? @State var selectedWaypoint: WaypointEntity? @State var newWaypointCoord: CLLocationCoordinate2D? @@ -54,7 +49,7 @@ struct MeshMap: View { ZStack { MapReader { reader in Map(position: $position, bounds: MapCameraBounds(minimumDistance: 1, maximumDistance: .infinity), scope: mapScope) { - MeshMapContent(routes: Array(routes), showUserLocation: $showUserLocation, showNodeHistory: $showNodeHistory, showRouteLines: $showRouteLines, showConvexHull: $showConvexHull, showTraffic: $showTraffic, showPointsOfInterest: $showPointsOfInterest, selectedMapLayer: $selectedMapLayer, selectedPosition: $selectedPosition, showWaypoints: $showWaypoints, selectedWaypoint: $selectedWaypoint) + MeshMapContent(routes: Array(routes), showUserLocation: $showUserLocation, showTraffic: $showTraffic, showPointsOfInterest: $showPointsOfInterest, selectedMapLayer: $selectedMapLayer, selectedPosition: $selectedPosition, selectedWaypoint: $selectedWaypoint) } .mapScope(mapScope) @@ -116,7 +111,7 @@ struct MeshMap: View { .padding() } .sheet(isPresented: $isEditingSettings) { - MapSettingsForm(nodeHistory: $showNodeHistory, routeLines: $showRouteLines, convexHull: $showConvexHull, traffic: $showTraffic, pointsOfInterest: $showPointsOfInterest, mapLayer: $selectedMapLayer, meshMapDistance: $meshMapDistance, meshMap: $isMeshMap) + MapSettingsForm(traffic: $showTraffic, pointsOfInterest: $showPointsOfInterest, mapLayer: $selectedMapLayer, meshMap: $isMeshMap) } .onChange(of: (appState.navigationPath)) { newPath in @@ -141,7 +136,7 @@ struct MeshMap: View { // print("Waypoint not found") // return // } - showWaypoints = true + //showWaypoints = true //position = .camera(MapCamera(centerCoordinate: waypoint.coordinate, distance: 1000, heading: 0, pitch: 60)) } } @@ -172,21 +167,7 @@ struct MeshMap: View { } .tint(Color(UIColor.secondarySystemBackground)) .foregroundColor(.accentColor) - .buttonStyle(.borderedProminent) - /// Show / Hide Waypoints Button - - Button(action: { - withAnimation { - showWaypoints = !showWaypoints - } - }) { - Image(systemName: showWaypoints ? "signpost.right.and.left.fill" : "signpost.right.and.left") - .padding(.vertical, 5) - } - .tint(Color(UIColor.secondarySystemBackground)) - .foregroundColor(.accentColor) - .buttonStyle(.borderedProminent) - + .buttonStyle(.borderedProminent) } .controlSize(.regular) .padding(5) From 525b1b2509ae985dabe57bd6ec898dbb9e077f9e Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Mon, 25 Mar 2024 19:20:36 -0700 Subject: [PATCH 17/34] More map cleanup --- .../Helpers/Map/MapContent/MeshMapContent.swift | 9 ++++----- .../Helpers/Map/MapContent/NodeMapContent.swift | 8 +++----- .../Views/Nodes/Helpers/Map/MapSettingsForm.swift | 15 ++++++++------- .../Views/Nodes/Helpers/Map/NodeMapSwiftUI.swift | 3 ++- Meshtastic/Views/Nodes/MeshMap.swift | 7 +------ 5 files changed, 18 insertions(+), 24 deletions(-) diff --git a/Meshtastic/Views/Nodes/Helpers/Map/MapContent/MeshMapContent.swift b/Meshtastic/Views/Nodes/Helpers/Map/MapContent/MeshMapContent.swift index e64febb5..19bcd6ca 100644 --- a/Meshtastic/Views/Nodes/Helpers/Map/MapContent/MeshMapContent.swift +++ b/Meshtastic/Views/Nodes/Helpers/Map/MapContent/MeshMapContent.swift @@ -8,14 +8,9 @@ import SwiftUI import MapKit -import SwiftUI -import MapKit - @available(iOS 17.0, macOS 14.0, *) struct MeshMapContent: MapContent { - //@State var waypoints: [WaypointEntity] = [] - @State var routes: [RouteEntity] = [] /// Parameters @Binding var showUserLocation: Bool @AppStorage("meshMapShowNodeHistory") private var showNodeHistory = false @@ -34,6 +29,10 @@ struct MeshMapContent: MapContent { @FetchRequest(fetchRequest: WaypointEntity.allWaypointssFetchRequest(), animation: .none) var waypoints: FetchedResults + + @FetchRequest(sortDescriptors: [NSSortDescriptor(key: "name", ascending: true)], + predicate: NSPredicate(format: "enabled == true", ""), animation: .none) + private var routes: FetchedResults var delay: Double = 0 @State private var scale: CGFloat = 0.5 diff --git a/Meshtastic/Views/Nodes/Helpers/Map/MapContent/NodeMapContent.swift b/Meshtastic/Views/Nodes/Helpers/Map/MapContent/NodeMapContent.swift index 8e8d0b26..7805e288 100644 --- a/Meshtastic/Views/Nodes/Helpers/Map/MapContent/NodeMapContent.swift +++ b/Meshtastic/Views/Nodes/Helpers/Map/MapContent/NodeMapContent.swift @@ -6,6 +6,7 @@ // import SwiftUI import MapKit +import CoreData @available(iOS 17.0, macOS 14.0, *) struct NodeMapContent: MapContent { @@ -16,10 +17,12 @@ struct NodeMapContent: MapContent { /// Map State User Defaults @AppStorage("meshMapShowNodeHistory") private var showNodeHistory = false @AppStorage("meshMapShowRouteLines") private var showRouteLines = false + @AppStorage("enableMapWaypoints") private var showWaypoints = false @AppStorage("enableMapConvexHull") private var showConvexHull = false @AppStorage("enableMapTraffic") private var showTraffic: Bool = false @AppStorage("enableMapPointsOfInterest") private var showPointsOfInterest: Bool = false @AppStorage("mapLayer") private var selectedMapLayer: MapLayer = .hybrid + // Map Configuration @Namespace var mapScope @State var mapStyle: MapStyle = MapStyle.hybrid(elevation: .realistic, pointsOfInterest: .all, showsTraffic: true) @@ -31,9 +34,6 @@ struct NodeMapContent: MapContent { @State var selectedPosition: PositionEntity? @State var isMeshMap = false - //let region: MKCoordinateRegion - - @MapContentBuilder var nodeMap: some MapContent { let positionArray = node.positions?.array as? [PositionEntity] ?? [] @@ -43,7 +43,6 @@ struct NodeMapContent: MapContent { /// Node Color from node.num let nodeColor = UIColor(hex: UInt32(node.num)) - /// Node Annotations ForEach(positionArray, id: \.id) { position in let pf = PositionFlags(rawValue: Int(position.nodePosition?.metadata?.positionFlags ?? 771)) @@ -79,7 +78,6 @@ struct NodeMapContent: MapContent { MapPolyline(coordinates: lineCoords) .stroke(gradient, style: dashed) } - /// Node Annotations ForEach(positionArray, id: \.id) { position in Annotation(position.latest ? node.user?.shortName ?? "?": "", coordinate: position.coordinate) { diff --git a/Meshtastic/Views/Nodes/Helpers/Map/MapSettingsForm.swift b/Meshtastic/Views/Nodes/Helpers/Map/MapSettingsForm.swift index 80d6a61f..9a775911 100644 --- a/Meshtastic/Views/Nodes/Helpers/Map/MapSettingsForm.swift +++ b/Meshtastic/Views/Nodes/Helpers/Map/MapSettingsForm.swift @@ -55,14 +55,15 @@ struct MapSettingsForm: View { .onChange(of: meshMapDistance) { newMeshMapDistance in UserDefaults.meshMapDistance = newMeshMapDistance } + Toggle(isOn: $waypoints) { + Label("Show Waypoints ", systemImage: "signpost.right.and.left") + } + .toggleStyle(SwitchToggleStyle(tint: .accentColor)) + .onTapGesture { + UserDefaults.enableMapWaypoints = !waypoints + } } - Toggle(isOn: $waypoints) { - Label("Show Waypoints ", systemImage: "signpost.right.and.left") - } - .toggleStyle(SwitchToggleStyle(tint: .accentColor)) - .onTapGesture { - UserDefaults.enableMapWaypoints = !waypoints - } + Toggle(isOn: $nodeHistory) { Label("Node History", systemImage: "building.columns.fill") } diff --git a/Meshtastic/Views/Nodes/Helpers/Map/NodeMapSwiftUI.swift b/Meshtastic/Views/Nodes/Helpers/Map/NodeMapSwiftUI.swift index bd512e94..6bb5c18c 100644 --- a/Meshtastic/Views/Nodes/Helpers/Map/NodeMapSwiftUI.swift +++ b/Meshtastic/Views/Nodes/Helpers/Map/NodeMapSwiftUI.swift @@ -31,7 +31,6 @@ struct NodeMapSwiftUI: View { @State var isLookingAround = false @State var isShowingAltitude = false @State var isEditingSettings = false - @State var selectedPosition: PositionEntity? @State var isMeshMap = false @State private var mapRegion = MKCoordinateRegion.init() @@ -42,6 +41,8 @@ struct NodeMapSwiftUI: View { ), animation: .none) private var waypoints: FetchedResults + + var body: some View { var mostRecent = node.positions?.lastObject as? PositionEntity diff --git a/Meshtastic/Views/Nodes/MeshMap.swift b/Meshtastic/Views/Nodes/MeshMap.swift index 0787ea55..2b17ab92 100644 --- a/Meshtastic/Views/Nodes/MeshMap.swift +++ b/Meshtastic/Views/Nodes/MeshMap.swift @@ -38,18 +38,13 @@ struct MeshMap: View { @State var newWaypointCoord: CLLocationCoordinate2D? @State var isMeshMap = true - - @FetchRequest(sortDescriptors: [NSSortDescriptor(key: "name", ascending: true)], - predicate: NSPredicate(format: "enabled == true", ""), animation: .none) - private var routes: FetchedResults - var body: some View { NavigationStack { ZStack { MapReader { reader in Map(position: $position, bounds: MapCameraBounds(minimumDistance: 1, maximumDistance: .infinity), scope: mapScope) { - MeshMapContent(routes: Array(routes), showUserLocation: $showUserLocation, showTraffic: $showTraffic, showPointsOfInterest: $showPointsOfInterest, selectedMapLayer: $selectedMapLayer, selectedPosition: $selectedPosition, selectedWaypoint: $selectedWaypoint) + MeshMapContent(showUserLocation: $showUserLocation, showTraffic: $showTraffic, showPointsOfInterest: $showPointsOfInterest, selectedMapLayer: $selectedMapLayer, selectedPosition: $selectedPosition, selectedWaypoint: $selectedWaypoint) } .mapScope(mapScope) From 496451c15caf551a60d79d5eddb5648591ae82fb Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Tue, 26 Mar 2024 07:54:16 -0700 Subject: [PATCH 18/34] Add node filters --- Meshtastic.xcodeproj/project.pbxproj | 4 + Meshtastic/Enums/AppSettingsEnums.swift | 2 +- Meshtastic/Enums/DeviceEnums.swift | 6 +- .../CoreData/PositionEntityExtension.swift | 1 - .../Map/MapContent/MeshMapContent.swift | 1 - .../Views/Nodes/Helpers/NodeListFilter.swift | 117 +++++++++ .../Views/Nodes/Helpers/NodeListItem.swift | 2 +- Meshtastic/Views/Nodes/NodeList.swift | 129 +++++----- .../Settings/Config/Module/MQTTConfig.swift | 2 +- Meshtastic/Views/Settings/Settings.swift | 226 +++++++++++------- 10 files changed, 344 insertions(+), 146 deletions(-) create mode 100644 Meshtastic/Views/Nodes/Helpers/NodeListFilter.swift diff --git a/Meshtastic.xcodeproj/project.pbxproj b/Meshtastic.xcodeproj/project.pbxproj index 67e51a8e..5e714267 100644 --- a/Meshtastic.xcodeproj/project.pbxproj +++ b/Meshtastic.xcodeproj/project.pbxproj @@ -186,6 +186,7 @@ DDDB445229F8ACF900EE2349 /* Date.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDDB445129F8ACF900EE2349 /* Date.swift */; }; DDDB445429F8AD1600EE2349 /* Data.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDDB445329F8AD1600EE2349 /* Data.swift */; }; DDDC22382BA92344002C44F1 /* MeshMapContent.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDDC22372BA92344002C44F1 /* MeshMapContent.swift */; }; + DDDCD5702BB26F5C00BE6B60 /* NodeListFilter.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDDCD56F2BB26F5C00BE6B60 /* NodeListFilter.swift */; }; DDDE59F529AF163D00490C6C /* WidgetKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = DD41A61C29AE7E8E003C5A37 /* WidgetKit.framework */; }; DDDE59F629AF163D00490C6C /* SwiftUI.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = DD41A61E29AE7E8F003C5A37 /* SwiftUI.framework */; }; DDDE59F929AF163D00490C6C /* WidgetsBundle.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDDE59F829AF163D00490C6C /* WidgetsBundle.swift */; }; @@ -458,6 +459,7 @@ DDDC22312BA76701002C44F1 /* fr */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = fr; path = fr.lproj/Localizable.strings; sourceTree = ""; }; DDDC22322BA76961002C44F1 /* zh-Hant-TW */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "zh-Hant-TW"; path = "zh-Hant-TW.lproj/Localizable.strings"; sourceTree = ""; }; DDDC22372BA92344002C44F1 /* MeshMapContent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MeshMapContent.swift; sourceTree = ""; }; + DDDCD56F2BB26F5C00BE6B60 /* NodeListFilter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NodeListFilter.swift; sourceTree = ""; }; DDDD527729B5B83F0045BC3C /* MeshtasticDataModelV9.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = MeshtasticDataModelV9.xcdatamodel; sourceTree = ""; }; DDDE59F429AF163D00490C6C /* WidgetsExtension.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = WidgetsExtension.appex; sourceTree = BUILT_PRODUCTS_DIR; }; DDDE59F829AF163D00490C6C /* WidgetsBundle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WidgetsBundle.swift; sourceTree = ""; }; @@ -960,6 +962,7 @@ DDDB26432AAC0206003AFCB7 /* NodeDetail.swift */, DDDB26452AACC0B7003AFCB7 /* NodeInfoItem.swift */, DDDB26412AABF655003AFCB7 /* NodeListItem.swift */, + DDDCD56F2BB26F5C00BE6B60 /* NodeListFilter.swift */, ); path = Helpers; sourceTree = ""; @@ -1355,6 +1358,7 @@ DDB6ABE228B13FB500384BA1 /* PositionConfigEnums.swift in Sources */, DD5E520E298EE33B00D21B61 /* mqtt.pb.swift in Sources */, DD994B69295F88B60013760A /* IntervalEnums.swift in Sources */, + DDDCD5702BB26F5C00BE6B60 /* NodeListFilter.swift in Sources */, DD1933762B0835D500771CD5 /* PositionAltitudeChart.swift in Sources */, DD415828285859C4009B0E59 /* TelemetryConfig.swift in Sources */, DDDB443D29F6592F00EE2349 /* NetworkManager.swift in Sources */, diff --git a/Meshtastic/Enums/AppSettingsEnums.swift b/Meshtastic/Enums/AppSettingsEnums.swift index 4169444e..0250119a 100644 --- a/Meshtastic/Enums/AppSettingsEnums.swift +++ b/Meshtastic/Enums/AppSettingsEnums.swift @@ -56,7 +56,7 @@ enum MeshMapDistances: Double, CaseIterable, Identifiable { case twoHundredMiles = 321869 case fiveHundredMiles = 804672 case oneThousandMiles = 1609000 - case twoThousandMiles = 3218688 + case twentyFiveHundredMiles = 4023360 var id: Double { self.rawValue } var description: String { let distanceFormatter = MKDistanceFormatter() diff --git a/Meshtastic/Enums/DeviceEnums.swift b/Meshtastic/Enums/DeviceEnums.swift index b754e986..8f2bbc7b 100644 --- a/Meshtastic/Enums/DeviceEnums.swift +++ b/Meshtastic/Enums/DeviceEnums.swift @@ -74,11 +74,13 @@ enum DeviceRoles: Int, CaseIterable, Identifiable { var systemName: String { switch self { case .client: - return "iphone.gen3.radiowaves.left.and.right" + return "apps.iphone" case .clientMute: return "speaker.slash" - case .router, .routerClient, .repeater: + case .router, .routerClient: return "wifi.router" + case .repeater: + return "repeat" case .tracker: return "mappin.and.ellipse.circle" case .sensor: diff --git a/Meshtastic/Extensions/CoreData/PositionEntityExtension.swift b/Meshtastic/Extensions/CoreData/PositionEntityExtension.swift index 20fbbd35..b24ec821 100644 --- a/Meshtastic/Extensions/CoreData/PositionEntityExtension.swift +++ b/Meshtastic/Extensions/CoreData/PositionEntityExtension.swift @@ -26,7 +26,6 @@ extension PositionEntity { let pointOfInterest = LocationHelper.currentLocation if pointOfInterest.latitude != LocationHelper.DefaultLocation.latitude && pointOfInterest.longitude != LocationHelper.DefaultLocation.longitude { - /// Lets just get nodes within about 500 miles let D: Double = UserDefaults.meshMapDistance * 1.1 let R: Double = 6371009 let meanLatitidue = pointOfInterest.latitude * .pi / 180 diff --git a/Meshtastic/Views/Nodes/Helpers/Map/MapContent/MeshMapContent.swift b/Meshtastic/Views/Nodes/Helpers/Map/MapContent/MeshMapContent.swift index 19bcd6ca..2655fbd4 100644 --- a/Meshtastic/Views/Nodes/Helpers/Map/MapContent/MeshMapContent.swift +++ b/Meshtastic/Views/Nodes/Helpers/Map/MapContent/MeshMapContent.swift @@ -205,6 +205,5 @@ struct MeshMapContent: MapContent { @MapContentBuilder var body: some MapContent { meshMap - } } diff --git a/Meshtastic/Views/Nodes/Helpers/NodeListFilter.swift b/Meshtastic/Views/Nodes/Helpers/NodeListFilter.swift new file mode 100644 index 00000000..03e04d84 --- /dev/null +++ b/Meshtastic/Views/Nodes/Helpers/NodeListFilter.swift @@ -0,0 +1,117 @@ +// +// NodeListFilter.swift +// Meshtastic +// +// Created by Garth Vander Houwen on 3/25/24. +// + +import Foundation +import SwiftUI + +struct NodeListFilter: View { + @Environment(\.dismiss) private var dismiss + /// Filters + @Binding var viaLora: Bool + @Binding var viaMqtt: Bool + @Binding var distanceFilter: Bool + @Binding var maximumDistance: Double + @Binding var hopsAway: Int + @Binding var deviceRole: Int + + var body: some View { + + NavigationStack { + Form { + Section(header: Text("Node Filters")) { + Toggle(isOn: $viaLora) { + + Label { + Text("Via Lora") + } icon: { + Image(systemName: "dot.radiowaves.left.and.right") + .rotationEffect(.degrees(-90)) + } + } + .toggleStyle(SwitchToggleStyle(tint: .accentColor)) + Toggle(isOn: $viaMqtt) { + + Label { + Text("Via Mqtt") + } icon: { + Image(systemName: "dot.radiowaves.up.forward") + } + } + .toggleStyle(SwitchToggleStyle(tint: .accentColor)) + .listRowSeparator(.visible) + +// Toggle(isOn: $distanceFilter) { +// +// Label { +// Text("Distance") +// } icon: { +// Image(systemName: "map") +// } +// } +// .toggleStyle(SwitchToggleStyle(tint: .accentColor)) +// +// .listRowSeparator(distanceFilter ? .hidden : .visible) +// if distanceFilter { +// HStack { +// Label("Show nodes", systemImage: "lines.measurement.horizontal") +// Picker("", selection: $maximumDistance) { +// ForEach(MeshMapDistances.allCases) { di in +// Text(di.description) +// .tag(di.id) +// } +// } +// .pickerStyle(DefaultPickerStyle()) +// } +// } + HStack { + Label("Hops Away", systemImage: "hare") + Picker("", selection: $hopsAway) { + Text("Any") + .tag(-1) + Text("Direct") + .tag(0) + ForEach(1..<8) { + Text("\($0)") + .tag($0) + } + } + .pickerStyle(DefaultPickerStyle()) + } + HStack { + Label("Device Role", systemImage: "apps.iphone") + Picker("", selection: $deviceRole) { + Text("All Roles") + .tag(-1) + ForEach(DeviceRoles.allCases) { dr in + Label { + Text(" \(dr.name)") + } icon: { + Image(systemName: dr.systemName) + } + } + } + .pickerStyle(DefaultPickerStyle()) + } + } + } +#if targetEnvironment(macCatalyst) + Spacer() + Button { + dismiss() + } label: { + Label("close", systemImage: "xmark") + } + .buttonStyle(.bordered) + .buttonBorderShape(.capsule) + .controlSize(.large) + .padding(.bottom) +#endif + } + .presentationDetents([.fraction(0.35), .fraction(0.45)]) + .presentationDragIndicator(.visible) + } +} diff --git a/Meshtastic/Views/Nodes/Helpers/NodeListItem.swift b/Meshtastic/Views/Nodes/Helpers/NodeListItem.swift index feb528f6..f7ea4ffc 100644 --- a/Meshtastic/Views/Nodes/Helpers/NodeListItem.swift +++ b/Meshtastic/Views/Nodes/Helpers/NodeListItem.swift @@ -132,7 +132,7 @@ struct NodeListItem: View { } if node.viaMqtt && connectedNode != node.num { - Image(systemName: "network") + Image(systemName: "dot.radiowaves.up.forward") .symbolRenderingMode(.hierarchical) .font(.callout) .frame(width: 30) diff --git a/Meshtastic/Views/Nodes/NodeList.swift b/Meshtastic/Views/Nodes/NodeList.swift index 902d7df5..4efad7a4 100644 --- a/Meshtastic/Views/Nodes/NodeList.swift +++ b/Meshtastic/Views/Nodes/NodeList.swift @@ -7,28 +7,6 @@ import SwiftUI import CoreLocation -struct NodeSearchState { - var searchText = "" - var searchScope = SearchScopes.all - var predicate: NSPredicate = .init() - - enum SearchScopes: CaseIterable, Identifiable { - case all - case lora - case mqtt - - var id: Self { self } - - var title: LocalizedStringKey { - switch self { - case .all: return "All" - case .lora: return "LoRa" - case .mqtt: return "MQTT" - } - } - } -} - struct NodeList: View { @State private var columnVisibility = NavigationSplitViewVisibility.all @@ -38,11 +16,17 @@ struct NodeList: View { @State private var isPresentingDeleteNodeAlert = false @State private var isPresentingPositionSentAlert = false @State private var deleteNodeId: Int64 = 0 - @State private var searchState = NodeSearchState() + @State private var searchText = "" + @State private var viaLora = true + @State private var viaMqtt = true + @State private var distanceFilter = false + @State private var maxDistance: Double = 800000 + @State private var hopsAway: Int = -1 + @State private var deviceRole: Int = -1 + + @State var isEditingFilters = false @SceneStorage("selectedDetailView") var selectedDetailView: String? - - @State private var searchText = "" @Environment(\.managedObjectContext) var context @EnvironmentObject var bleManager: BLEManager @@ -159,14 +143,17 @@ struct NodeList: View { Text("Any missed messages will be delivered again.") } } + .sheet(isPresented: $isEditingFilters) { + NodeListFilter(viaLora: $viaLora, viaMqtt: $viaMqtt, distanceFilter: $distanceFilter, maximumDistance: $maxDistance, hopsAway: $hopsAway, deviceRole: $deviceRole) + } .safeAreaInset(edge: .bottom, alignment: .trailing) { HStack { Button(action: { withAnimation { - //isEditingSettings = !isEditingSettings + isEditingFilters = !isEditingFilters } }) { - Image(systemName: true ? "line.3.horizontal.decrease.circle" : "line.3.horizontal.decrease.circle.fill") + Image(systemName: !isEditingFilters ? "line.3.horizontal.decrease.circle" : "line.3.horizontal.decrease.circle.fill") .padding(.vertical, 5) } .tint(Color(UIColor.secondarySystemBackground)) @@ -178,14 +165,9 @@ struct NodeList: View { .padding(5) } .padding(.bottom, 5) - .searchable(text: $searchState.searchText, placement: nodes.count > 10 ? .navigationBarDrawer(displayMode: .always) : .automatic, prompt: "Find a node") + .searchable(text: $searchText, placement: .automatic, prompt: "Find a node") .disableAutocorrection(true) .scrollDismissesKeyboard(.immediately) - .searchScopes($searchState.searchScope) { - ForEach(NodeSearchState.SearchScopes.allCases) { scope in - Text(scope.title).tag(scope) - } - } .navigationTitle(String.localizedStringWithFormat("nodes %@".localized, String(nodes.count))) .listStyle(.plain) .confirmationDialog( @@ -256,10 +238,19 @@ struct NodeList: View { } .navigationSplitViewStyle(.balanced) - .onChange(of: searchState.searchText) { _ in + .onChange(of: searchText) { _ in searchNodeList() } - .onChange(of: searchState.searchScope) { _ in + .onChange(of: viaLora) { _ in + searchNodeList() + } + .onChange(of: viaMqtt) { _ in + searchNodeList() + } + .onChange(of: deviceRole) { _ in + searchNodeList() + } + .onChange(of: hopsAway) { _ in searchNodeList() } .onAppear { @@ -272,35 +263,63 @@ struct NodeList: View { private func searchNodeList() { /// Case Insensitive Search Text Predicates let searchPredicates = ["user.userId", "user.hwModel", "user.longName", "user.shortName"].map { property in - return NSPredicate(format: "%K CONTAINS[c] %@", property, searchState.searchText) + return NSPredicate(format: "%K CONTAINS[c] %@", property, searchText) } /// Create a compound predicate using each text search preicate as an OR let textSearchPredicate = NSCompoundPredicate(type: .or, subpredicates: searchPredicates) + /// Create an array of predicates to hold our AND predicates + var predicates: [NSPredicate] = [] + /// Mqtt + if !(viaLora && viaMqtt) { + if viaLora { + let loraPredicate = NSPredicate(format: "viaMqtt == NO") + predicates.append(loraPredicate) + } else { + let mqttPredicate = NSPredicate(format: "viaMqtt == YES") + predicates.append(mqttPredicate) + } + } + /// Role + if deviceRole > 0 { + let rolePredicate = NSPredicate(format: "user.role == %i", Int32(deviceRole)) + predicates.append(rolePredicate) + } + /// Hops Away + if hopsAway > 0 { + let hopsAwayPredicate = NSPredicate(format: "hopsAway == %i", Int32(hopsAway)) + predicates.append(hopsAwayPredicate) + } + /// Distance + if distanceFilter { + let pointOfInterest = LocationHelper.currentLocation - /// Set the predicate to nil if the search string is empty - if searchState.searchText.isEmpty { - nodes.nsPredicate = nil - return + if pointOfInterest.latitude != LocationHelper.DefaultLocation.latitude && pointOfInterest.longitude != LocationHelper.DefaultLocation.longitude { + let D: Double = maxDistance * 1.1 + let R: Double = 6371009 + let meanLatitidue = pointOfInterest.latitude * .pi / 180 + let deltaLatitude = D / R * 180 / .pi + let deltaLongitude = D / (R * cos(meanLatitidue)) * 180 / .pi + let minLatitude: Double = pointOfInterest.latitude - deltaLatitude + let maxLatitude: Double = pointOfInterest.latitude + deltaLatitude + let minLongitude: Double = pointOfInterest.longitude - deltaLongitude + let maxLongitude: Double = pointOfInterest.longitude + deltaLongitude + let distancePredicate = NSPredicate(format: "(%lf <= (positions[first].longitudeI / 1e7))", minLongitude, maxLongitude,minLatitude, maxLatitude) + //let distancePredicate = NSPredicate(format: "(%lf <= (positions[LAST].longitudeI / 1e7)) AND ((positions[LAST].longitudeI / 1e7) <= %lf) AND (%lf <= (positions[LAST].latitudeI / 1e7)) AND ((positions[LAST].latitudeI / 1e7) <= %lf)", minLongitude, maxLongitude,minLatitude, maxLatitude) + + //predicates.append(distancePredicate) + } } - /// Add a predicate for the search scope if selected - if searchState.searchScope != .all { + if predicates.count > 0 { - if searchState.searchScope == .lora { - let loraPredicate = NSPredicate(format: "viaMqtt == NO") - let scopePredicate = NSCompoundPredicate(type: .and, subpredicates: [loraPredicate]) - nodes.nsPredicate = NSCompoundPredicate(type: .and, subpredicates: [textSearchPredicate, scopePredicate]) - return - - } else if searchState.searchScope == .mqtt { - let mqttPredicate = NSPredicate(format: "viaMqtt == YES") - let scopePredicate = NSCompoundPredicate(type: .and, subpredicates: [mqttPredicate]) - nodes.nsPredicate = NSCompoundPredicate(type: .and, subpredicates: [textSearchPredicate, scopePredicate]) - return + if !searchText.isEmpty { + let filterPredicates = NSCompoundPredicate(type: .and, subpredicates: predicates) + nodes.nsPredicate = NSCompoundPredicate(type: .and, subpredicates: [textSearchPredicate, filterPredicates]) + } else { + nodes.nsPredicate = NSCompoundPredicate(type: .and, subpredicates: predicates) } } else { - /// Use the text search predicate - nodes.nsPredicate = textSearchPredicate + nodes.nsPredicate = nil } } } diff --git a/Meshtastic/Views/Settings/Config/Module/MQTTConfig.swift b/Meshtastic/Views/Settings/Config/Module/MQTTConfig.swift index cfcaa930..8191aec9 100644 --- a/Meshtastic/Views/Settings/Config/Module/MQTTConfig.swift +++ b/Meshtastic/Views/Settings/Config/Module/MQTTConfig.swift @@ -47,7 +47,7 @@ struct MQTTConfig: View { Section(header: Text("options")) { Toggle(isOn: $enabled) { - Label("enabled", systemImage: "dot.radiowaves.right") + Label("enabled", systemImage: "dot.radiowaves.up.forward") } .toggleStyle(SwitchToggleStyle(tint: .accentColor)) diff --git a/Meshtastic/Views/Settings/Settings.swift b/Meshtastic/Views/Settings/Settings.swift index b98893e9..b1883c1c 100644 --- a/Meshtastic/Views/Settings/Settings.swift +++ b/Meshtastic/Views/Settings/Settings.swift @@ -50,34 +50,43 @@ struct Settings: View { NavigationLink { AboutMeshtastic() } label: { - Image(systemName: "questionmark.app") - .symbolRenderingMode(.hierarchical) - Text("about.meshtastic") + Label { + Text("about.meshtastic") + } icon: { + Image(systemName: "questionmark.app") + } } .tag(SettingsSidebar.about) NavigationLink { AppSettings() } label: { - Image(systemName: "gearshape") - .symbolRenderingMode(.hierarchical) - Text("appsettings") + Label { + Text("appsettings") + } icon: { + Image(systemName: "gearshape") + } } .tag(SettingsSidebar.appSettings) if #available(iOS 17.0, macOS 14.0, *) { NavigationLink { Routes() } label: { - Image(systemName: "road.lanes.curved.right") - .symbolRenderingMode(.hierarchical) - Text("routes") + Label { + Text("routes") + } icon: { + Image(systemName: "road.lanes.curved.right") + } } .tag(SettingsSidebar.routes) NavigationLink { RouteRecorder() } label: { - Image(systemName: "record.circle") - .symbolRenderingMode(.hierarchical) - Text("route.recorder") + Label { + Text("route.recorder") + } icon: { + Image(systemName: "record.circle") + .foregroundColor(.red) + } } .tag(SettingsSidebar.routeRecorder) } @@ -152,26 +161,33 @@ struct Settings: View { NavigationLink { LoRaConfig(node: nodes.first(where: { $0.num == selectedNode })) } label: { - Image(systemName: "dot.radiowaves.left.and.right") - .symbolRenderingMode(.hierarchical) - Text("lora") + Label { + Text("lora") + } icon: { + Image(systemName: "dot.radiowaves.left.and.right") + .rotationEffect(.degrees(-90)) + } } .tag(SettingsSidebar.loraConfig) NavigationLink { Channels(node: nodes.first(where: { $0.num == preferredNodeNum })) } label: { - Image(systemName: "fibrechannel") - .symbolRenderingMode(.hierarchical) - Text("channels") + Label { + Text("channels") + } icon: { + Image(systemName: "fibrechannel") + } } .tag(SettingsSidebar.channelConfig) .disabled(selectedNode > 0 && selectedNode != preferredNodeNum) NavigationLink { ShareChannels(node: nodes.first(where: { $0.num == preferredNodeNum })) } label: { - Image(systemName: "qrcode") - .symbolRenderingMode(.hierarchical) - Text("share.channels") + Label { + Text("share.channels") + } icon: { + Image(systemName: "qrcode") + } } .tag(SettingsSidebar.shareChannels) .disabled(selectedNode > 0 && selectedNode != preferredNodeNum) @@ -180,58 +196,72 @@ struct Settings: View { NavigationLink { UserConfig(node: nodes.first(where: { $0.num == selectedNode })) } label: { - Image(systemName: "person.crop.rectangle.fill") - .symbolRenderingMode(.hierarchical) - Text("user") + Label { + Text("user") + } icon: { + Image(systemName: "person.crop.rectangle.fill") + } } .tag(SettingsSidebar.userConfig) NavigationLink { BluetoothConfig(node: nodes.first(where: { $0.num == selectedNode })) } label: { - Image(systemName: "antenna.radiowaves.left.and.right") - .symbolRenderingMode(.hierarchical) - Text("bluetooth") + Label { + Text("bluetooth") + } icon: { + Image(systemName: "antenna.radiowaves.left.and.right") + } } .tag(SettingsSidebar.bluetoothConfig) NavigationLink { DeviceConfig(node: nodes.first(where: { $0.num == selectedNode })) } label: { - Image(systemName: "flipphone") - .symbolRenderingMode(.hierarchical) - Text("device") + Label { + Text("device") + } icon: { + Image(systemName: "flipphone") + } } .tag(SettingsSidebar.deviceConfig) NavigationLink { DisplayConfig(node: nodes.first(where: { $0.num == selectedNode })) } label: { - Image(systemName: "display") - .symbolRenderingMode(.hierarchical) - Text("display") + Label { + Text("display") + } icon: { + Image(systemName: "display") + } } .tag(SettingsSidebar.displayConfig) NavigationLink { NetworkConfig(node: nodes.first(where: { $0.num == selectedNode })) } label: { - Image(systemName: "network") - .symbolRenderingMode(.hierarchical) - Text("network") + Label { + Text("network") + } icon: { + Image(systemName: "network") + } } .tag(SettingsSidebar.networkConfig) NavigationLink { PositionConfig(node: nodes.first(where: { $0.num == selectedNode })) } label: { - Image(systemName: "location") - .symbolRenderingMode(.hierarchical) - Text("position") + Label { + Text("position") + } icon: { + Image(systemName: "location") + } } .tag(SettingsSidebar.positionConfig) NavigationLink { PowerConfig(node: nodes.first(where: { $0.num == selectedNode })) } label: { - Image(systemName: "bolt.fill") - .symbolRenderingMode(.hierarchical) - Text("config.power.settings") + Label { + Text("config.power.settings") + } icon: { + Image(systemName: "bolt.fill") + } } .tag(SettingsSidebar.powerConfig) } @@ -240,92 +270,114 @@ struct Settings: View { NavigationLink { AmbientLightingConfig(node: nodes.first(where: { $0.num == selectedNode })) } label: { - Image(systemName: "light.max") - .symbolRenderingMode(.hierarchical) - Text("ambient.lighting") + Label { + Text("ambient.lighting") + } icon: { + Image(systemName: "light.max") + } } .tag(SettingsSidebar.ambientLightingConfig) } NavigationLink { CannedMessagesConfig(node: nodes.first(where: { $0.num == selectedNode })) } label: { - Image(systemName: "list.bullet.rectangle.fill") - .symbolRenderingMode(.hierarchical) - Text("canned.messages") + Label { + Text("canned.messages") + } icon: { + Image(systemName: "list.bullet.rectangle.fill") + } } .tag(SettingsSidebar.cannedMessagesConfig) NavigationLink { DetectionSensorConfig(node: nodes.first(where: { $0.num == selectedNode })) } label: { - Image(systemName: "sensor") - .symbolRenderingMode(.hierarchical) - Text("detection.sensor") + Label { + Text("detection.sensor") + } icon: { + Image(systemName: "sensor") + } } .tag(SettingsSidebar.detectionSensorConfig) NavigationLink { ExternalNotificationConfig(node: nodes.first(where: { $0.num == selectedNode })) } label: { - Image(systemName: "megaphone") - .symbolRenderingMode(.hierarchical) - Text("external.notification") + Label { + Text("external.notification") + } icon: { + Image(systemName: "megaphone") + } } .tag(SettingsSidebar.externalNotificationConfig) NavigationLink { MQTTConfig(node: nodes.first(where: { $0.num == selectedNode })) } label: { - Image(systemName: "dot.radiowaves.right") - .symbolRenderingMode(.hierarchical) - Text("mqtt") + Label { + Text("mqtt") + } icon: { + Image(systemName: "dot.radiowaves.up.forward") + } } .tag(SettingsSidebar.mqttConfig) NavigationLink { RangeTestConfig(node: nodes.first(where: { $0.num == selectedNode })) } label: { - Image(systemName: "point.3.connected.trianglepath.dotted") - .symbolRenderingMode(.hierarchical) - Text("range.test") + Label { + Text("range.test") + } icon: { + Image(systemName: "point.3.connected.trianglepath.dotted") + } } .tag(SettingsSidebar.rangeTestConfig) if node?.metadata?.hasWifi ?? false { NavigationLink { PaxCounterConfig(node: nodes.first(where: { $0.num == selectedNode })) } label: { - Image(systemName: "figure.walk.motion") - .symbolRenderingMode(.hierarchical) - Text("config.module.paxcounter.settings") + Label { + Text("config.module.paxcounter.setting") + } icon: { + Image(systemName: "figure.walk.motion") + } } .tag(SettingsSidebar.paxCounterConfig) } NavigationLink { RtttlConfig(node: nodes.first(where: { $0.num == selectedNode })) } label: { - Image(systemName: "music.note.list") - .symbolRenderingMode(.hierarchical) - Text("ringtone") + Label { + Text("ringtone") + } icon: { + Image(systemName: "music.note.list") + } } .tag(SettingsSidebar.ringtoneConfig) NavigationLink { SerialConfig(node: nodes.first(where: { $0.num == selectedNode })) } label: { - Image(systemName: "terminal") - .symbolRenderingMode(.hierarchical) - Text("serial") + Label { + Text("serial") + } icon: { + Image(systemName: "terminal") + } } .tag(SettingsSidebar.serialConfig) NavigationLink { StoreForwardConfig(node: nodes.first(where: { $0.num == selectedNode })) } label: { - Image(systemName: "envelope.arrow.triangle.branch") - .symbolRenderingMode(.hierarchical) - Text("storeforward") + Label { + Text("storeforward") + } icon: { + Image(systemName: "envelope.arrow.triangle.branch") + } } .tag(SettingsSidebar.storeAndForwardConfig) NavigationLink { TelemetryConfig(node: nodes.first(where: { $0.num == selectedNode })) } label: { - Image(systemName: "chart.xyaxis.line") - .symbolRenderingMode(.hierarchical) - Text("telemetry") + Label { + Text("telemetry") + } icon: { + Image(systemName: "chart.xyaxis.line") + } } .tag(SettingsSidebar.telemetryConfig) } @@ -333,18 +385,22 @@ struct Settings: View { NavigationLink { MeshLog() } label: { - Image(systemName: "list.bullet.rectangle") - .symbolRenderingMode(.hierarchical) - Text("mesh.log") + Label { + Text("mesh.log") + } icon: { + Image(systemName: "list.bullet.rectangle") + } } .tag(SettingsSidebar.meshLog) NavigationLink { let connectedNode = nodes.first(where: { $0.num == preferredNodeNum }) AdminMessageList(user: connectedNode?.user) } label: { - Image(systemName: "building.columns") - .symbolRenderingMode(.hierarchical) - Text("admin.log") + Label { + Text("admin.log") + } icon: { + Image(systemName: "building.columns") + } } .tag(SettingsSidebar.adminMessageLog) } @@ -352,9 +408,11 @@ struct Settings: View { NavigationLink { Firmware(node: nodes.first(where: { $0.num == preferredNodeNum })) } label: { - Image(systemName: "arrow.up.arrow.down.square") - .symbolRenderingMode(.hierarchical) - Text("Firmware Updates") + Label { + Text("Firmware Updates") + } icon: { + Image(systemName: "arrow.up.arrow.down.square") + } } .tag(SettingsSidebar.about) .disabled(selectedNode > 0 && selectedNode != preferredNodeNum) From 4f3cd0f72bd91f315f344a4c8cd94db4853ed099 Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Tue, 26 Mar 2024 08:55:38 -0700 Subject: [PATCH 19/34] Add TAK tracker, bump db version --- Meshtastic.xcodeproj/project.pbxproj | 4 +- Meshtastic/Enums/DeviceEnums.swift | 10 + .../Meshtastic.xcdatamodeld/.xccurrentversion | 2 +- .../contents | 457 ++++++++++++++++++ .../Protobufs/meshtastic/admin.pb.swift | 125 +++++ .../Protobufs/meshtastic/deviceonly.pb.swift | 72 --- Meshtastic/Protobufs/meshtastic/mesh.pb.swift | 72 +++ protobufs | 2 +- 8 files changed, 669 insertions(+), 75 deletions(-) create mode 100644 Meshtastic/Meshtastic.xcdatamodeld/MeshtasticDataModelV 31.xcdatamodel/contents diff --git a/Meshtastic.xcodeproj/project.pbxproj b/Meshtastic.xcodeproj/project.pbxproj index 5e714267..5cdd8853 100644 --- a/Meshtastic.xcodeproj/project.pbxproj +++ b/Meshtastic.xcodeproj/project.pbxproj @@ -460,6 +460,7 @@ DDDC22322BA76961002C44F1 /* zh-Hant-TW */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "zh-Hant-TW"; path = "zh-Hant-TW.lproj/Localizable.strings"; sourceTree = ""; }; DDDC22372BA92344002C44F1 /* MeshMapContent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MeshMapContent.swift; sourceTree = ""; }; DDDCD56F2BB26F5C00BE6B60 /* NodeListFilter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NodeListFilter.swift; sourceTree = ""; }; + DDDCD5712BB3246500BE6B60 /* MeshtasticDataModelV 31.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = "MeshtasticDataModelV 31.xcdatamodel"; sourceTree = ""; }; DDDD527729B5B83F0045BC3C /* MeshtasticDataModelV9.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = MeshtasticDataModelV9.xcdatamodel; sourceTree = ""; }; DDDE59F429AF163D00490C6C /* WidgetsExtension.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = WidgetsExtension.appex; sourceTree = BUILT_PRODUCTS_DIR; }; DDDE59F829AF163D00490C6C /* WidgetsBundle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WidgetsBundle.swift; sourceTree = ""; }; @@ -1907,6 +1908,7 @@ DD3CC6BA28E366DF00FA9159 /* Meshtastic.xcdatamodeld */ = { isa = XCVersionGroup; children = ( + DDDCD5712BB3246500BE6B60 /* MeshtasticDataModelV 31.xcdatamodel */, DD9A1A912BA2D2D3001E602E /* MeshtasticDataModelV 30.xcdatamodel */, DD398EBD2B93F640002B4C51 /* MeshtasticDataModelV 29.xcdatamodel */, DD0E20FF2B892E1300F2D100 /* MeshtasticDataModelV 28.xcdatamodel */, @@ -1938,7 +1940,7 @@ DD5D0A9A2931AD6B00F7EA61 /* MeshtasticDataModelV2.xcdatamodel */, DD3CC6BB28E366DF00FA9159 /* MeshtasticDataModel.xcdatamodel */, ); - currentVersion = DD9A1A912BA2D2D3001E602E /* MeshtasticDataModelV 30.xcdatamodel */; + currentVersion = DDDCD5712BB3246500BE6B60 /* MeshtasticDataModelV 31.xcdatamodel */; name = Meshtastic.xcdatamodeld; path = Meshtastic/Meshtastic.xcdatamodeld; sourceTree = ""; diff --git a/Meshtastic/Enums/DeviceEnums.swift b/Meshtastic/Enums/DeviceEnums.swift index 8f2bbc7b..dbbc6dc2 100644 --- a/Meshtastic/Enums/DeviceEnums.swift +++ b/Meshtastic/Enums/DeviceEnums.swift @@ -17,6 +17,7 @@ enum DeviceRoles: Int, CaseIterable, Identifiable { case lostAndFound = 9 case sensor = 6 case tak = 7 + case takTracker = 10 case repeater = 4 case router = 2 case routerClient = 3 @@ -40,11 +41,14 @@ enum DeviceRoles: Int, CaseIterable, Identifiable { return "Sensor" case .tak: return "TAK" + case .takTracker: + return "TAK Tracker" case .clientHidden: return "Client Hidden" case .lostAndFound: return "Lost and Found" } + } var description: String { switch self { @@ -64,6 +68,8 @@ enum DeviceRoles: Int, CaseIterable, Identifiable { return "device.role.sensor".localized case .tak: return "device.role.tak".localized + case .takTracker: + return "device.role.taktracker".localized case .clientHidden: return "device.role.clienthidden".localized case .lostAndFound: @@ -87,6 +93,8 @@ enum DeviceRoles: Int, CaseIterable, Identifiable { return "sensor" case .tak: return "shield.checkered" + case .takTracker: + return "dog" case .clientHidden: return "eye.slash" case .lostAndFound: @@ -112,6 +120,8 @@ enum DeviceRoles: Int, CaseIterable, Identifiable { return Config.DeviceConfig.Role.sensor case .tak: return Config.DeviceConfig.Role.tak + case .takTracker: + return Config.DeviceConfig.Role.takTracker case .clientHidden: return Config.DeviceConfig.Role.clientHidden case .lostAndFound: diff --git a/Meshtastic/Meshtastic.xcdatamodeld/.xccurrentversion b/Meshtastic/Meshtastic.xcdatamodeld/.xccurrentversion index e8a31698..825d8915 100644 --- a/Meshtastic/Meshtastic.xcdatamodeld/.xccurrentversion +++ b/Meshtastic/Meshtastic.xcdatamodeld/.xccurrentversion @@ -3,6 +3,6 @@ _XCCurrentVersionName - MeshtasticDataModelV 30.xcdatamodel + MeshtasticDataModelV 31.xcdatamodel diff --git a/Meshtastic/Meshtastic.xcdatamodeld/MeshtasticDataModelV 31.xcdatamodel/contents b/Meshtastic/Meshtastic.xcdatamodeld/MeshtasticDataModelV 31.xcdatamodel/contents new file mode 100644 index 00000000..72ba98d4 --- /dev/null +++ b/Meshtastic/Meshtastic.xcdatamodeld/MeshtasticDataModelV 31.xcdatamodel/contents @@ -0,0 +1,457 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/Meshtastic/Protobufs/meshtastic/admin.pb.swift b/Meshtastic/Protobufs/meshtastic/admin.pb.swift index baa3c742..fa1ac990 100644 --- a/Meshtastic/Protobufs/meshtastic/admin.pb.swift +++ b/Meshtastic/Protobufs/meshtastic/admin.pb.swift @@ -319,6 +319,46 @@ struct AdminMessage { set {payloadVariant = .removeByNodenum(newValue)} } + /// + /// Set specified node-num to be favorited on the NodeDB on the device + var setFavoriteNode: UInt32 { + get { + if case .setFavoriteNode(let v)? = payloadVariant {return v} + return 0 + } + set {payloadVariant = .setFavoriteNode(newValue)} + } + + /// + /// Set specified node-num to be un-favorited on the NodeDB on the device + var removeFavoriteNode: UInt32 { + get { + if case .removeFavoriteNode(let v)? = payloadVariant {return v} + return 0 + } + set {payloadVariant = .removeFavoriteNode(newValue)} + } + + /// + /// Set fixed position data on the node and then set the position.fixed_position = true + var setFixedPosition: Position { + get { + if case .setFixedPosition(let v)? = payloadVariant {return v} + return Position() + } + set {payloadVariant = .setFixedPosition(newValue)} + } + + /// + /// Clear fixed position coordinates and then set position.fixed_position = false + var removeFixedPosition: Bool { + get { + if case .removeFixedPosition(let v)? = payloadVariant {return v} + return false + } + set {payloadVariant = .removeFixedPosition(newValue)} + } + /// /// Begins an edit transaction for config, module config, owner, and channel settings changes /// This will delay the standard *implicit* save to the file system and subsequent reboot behavior until committed (commit_edit_settings) @@ -498,6 +538,18 @@ struct AdminMessage { /// Remove the node by the specified node-num from the NodeDB on the device case removeByNodenum(UInt32) /// + /// Set specified node-num to be favorited on the NodeDB on the device + case setFavoriteNode(UInt32) + /// + /// Set specified node-num to be un-favorited on the NodeDB on the device + case removeFavoriteNode(UInt32) + /// + /// Set fixed position data on the node and then set the position.fixed_position = true + case setFixedPosition(Position) + /// + /// Clear fixed position coordinates and then set position.fixed_position = false + case removeFixedPosition(Bool) + /// /// Begins an edit transaction for config, module config, owner, and channel settings changes /// This will delay the standard *implicit* save to the file system and subsequent reboot behavior until committed (commit_edit_settings) case beginEditSettings(Bool) @@ -643,6 +695,22 @@ struct AdminMessage { guard case .removeByNodenum(let l) = lhs, case .removeByNodenum(let r) = rhs else { preconditionFailure() } return l == r }() + case (.setFavoriteNode, .setFavoriteNode): return { + guard case .setFavoriteNode(let l) = lhs, case .setFavoriteNode(let r) = rhs else { preconditionFailure() } + return l == r + }() + case (.removeFavoriteNode, .removeFavoriteNode): return { + guard case .removeFavoriteNode(let l) = lhs, case .removeFavoriteNode(let r) = rhs else { preconditionFailure() } + return l == r + }() + case (.setFixedPosition, .setFixedPosition): return { + guard case .setFixedPosition(let l) = lhs, case .setFixedPosition(let r) = rhs else { preconditionFailure() } + return l == r + }() + case (.removeFixedPosition, .removeFixedPosition): return { + guard case .removeFixedPosition(let l) = lhs, case .removeFixedPosition(let r) = rhs else { preconditionFailure() } + return l == r + }() case (.beginEditSettings, .beginEditSettings): return { guard case .beginEditSettings(let l) = lhs, case .beginEditSettings(let r) = rhs else { preconditionFailure() } return l == r @@ -978,6 +1046,10 @@ extension AdminMessage: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementat 36: .standard(proto: "set_canned_message_module_messages"), 37: .standard(proto: "set_ringtone_message"), 38: .standard(proto: "remove_by_nodenum"), + 39: .standard(proto: "set_favorite_node"), + 40: .standard(proto: "remove_favorite_node"), + 41: .standard(proto: "set_fixed_position"), + 42: .standard(proto: "remove_fixed_position"), 64: .standard(proto: "begin_edit_settings"), 65: .standard(proto: "commit_edit_settings"), 95: .standard(proto: "reboot_ota_seconds"), @@ -1278,6 +1350,43 @@ extension AdminMessage: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementat self.payloadVariant = .removeByNodenum(v) } }() + case 39: try { + var v: UInt32? + try decoder.decodeSingularUInt32Field(value: &v) + if let v = v { + if self.payloadVariant != nil {try decoder.handleConflictingOneOf()} + self.payloadVariant = .setFavoriteNode(v) + } + }() + case 40: try { + var v: UInt32? + try decoder.decodeSingularUInt32Field(value: &v) + if let v = v { + if self.payloadVariant != nil {try decoder.handleConflictingOneOf()} + self.payloadVariant = .removeFavoriteNode(v) + } + }() + case 41: try { + var v: Position? + var hadOneofValue = false + if let current = self.payloadVariant { + hadOneofValue = true + if case .setFixedPosition(let m) = current {v = m} + } + try decoder.decodeSingularMessageField(value: &v) + if let v = v { + if hadOneofValue {try decoder.handleConflictingOneOf()} + self.payloadVariant = .setFixedPosition(v) + } + }() + case 42: try { + var v: Bool? + try decoder.decodeSingularBoolField(value: &v) + if let v = v { + if self.payloadVariant != nil {try decoder.handleConflictingOneOf()} + self.payloadVariant = .removeFixedPosition(v) + } + }() case 64: try { var v: Bool? try decoder.decodeSingularBoolField(value: &v) @@ -1465,6 +1574,22 @@ extension AdminMessage: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementat guard case .removeByNodenum(let v)? = self.payloadVariant else { preconditionFailure() } try visitor.visitSingularUInt32Field(value: v, fieldNumber: 38) }() + case .setFavoriteNode?: try { + guard case .setFavoriteNode(let v)? = self.payloadVariant else { preconditionFailure() } + try visitor.visitSingularUInt32Field(value: v, fieldNumber: 39) + }() + case .removeFavoriteNode?: try { + guard case .removeFavoriteNode(let v)? = self.payloadVariant else { preconditionFailure() } + try visitor.visitSingularUInt32Field(value: v, fieldNumber: 40) + }() + case .setFixedPosition?: try { + guard case .setFixedPosition(let v)? = self.payloadVariant else { preconditionFailure() } + try visitor.visitSingularMessageField(value: v, fieldNumber: 41) + }() + case .removeFixedPosition?: try { + guard case .removeFixedPosition(let v)? = self.payloadVariant else { preconditionFailure() } + try visitor.visitSingularBoolField(value: v, fieldNumber: 42) + }() case .beginEditSettings?: try { guard case .beginEditSettings(let v)? = self.payloadVariant else { preconditionFailure() } try visitor.visitSingularBoolField(value: v, fieldNumber: 64) diff --git a/Meshtastic/Protobufs/meshtastic/deviceonly.pb.swift b/Meshtastic/Protobufs/meshtastic/deviceonly.pb.swift index f25a5cd1..3080ae9f 100644 --- a/Meshtastic/Protobufs/meshtastic/deviceonly.pb.swift +++ b/Meshtastic/Protobufs/meshtastic/deviceonly.pb.swift @@ -405,35 +405,6 @@ struct OEMStore { fileprivate var _oemLocalModuleConfig: LocalModuleConfig? = nil } -/// -/// RemoteHardwarePins associated with a node -struct NodeRemoteHardwarePin { - // 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. - - /// - /// The node_num exposing the available gpio pin - var nodeNum: UInt32 = 0 - - /// - /// The the available gpio pin for usage with RemoteHardware module - var pin: RemoteHardwarePin { - get {return _pin ?? RemoteHardwarePin()} - set {_pin = newValue} - } - /// Returns true if `pin` has been explicitly set. - var hasPin: Bool {return self._pin != nil} - /// Clears the value of `pin`. Subsequent reads from it will return its default value. - mutating func clearPin() {self._pin = nil} - - var unknownFields = SwiftProtobuf.UnknownStorage() - - init() {} - - fileprivate var _pin: RemoteHardwarePin? = nil -} - #if swift(>=5.5) && canImport(_Concurrency) extension ScreenFonts: @unchecked Sendable {} extension DeviceState: @unchecked Sendable {} @@ -441,7 +412,6 @@ extension NodeInfoLite: @unchecked Sendable {} extension PositionLite: @unchecked Sendable {} extension ChannelFile: @unchecked Sendable {} extension OEMStore: @unchecked Sendable {} -extension NodeRemoteHardwarePin: @unchecked Sendable {} #endif // swift(>=5.5) && canImport(_Concurrency) // MARK: - Code below here is support for the SwiftProtobuf runtime. @@ -907,45 +877,3 @@ extension OEMStore: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationB return true } } - -extension NodeRemoteHardwarePin: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { - static let protoMessageName: String = _protobuf_package + ".NodeRemoteHardwarePin" - static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ - 1: .standard(proto: "node_num"), - 2: .same(proto: "pin"), - ] - - mutating func decodeMessage(decoder: inout D) throws { - 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.decodeSingularUInt32Field(value: &self.nodeNum) }() - case 2: try { try decoder.decodeSingularMessageField(value: &self._pin) }() - 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.nodeNum != 0 { - try visitor.visitSingularUInt32Field(value: self.nodeNum, fieldNumber: 1) - } - try { if let v = self._pin { - try visitor.visitSingularMessageField(value: v, fieldNumber: 2) - } }() - try unknownFields.traverse(visitor: &visitor) - } - - static func ==(lhs: NodeRemoteHardwarePin, rhs: NodeRemoteHardwarePin) -> Bool { - if lhs.nodeNum != rhs.nodeNum {return false} - if lhs._pin != rhs._pin {return false} - if lhs.unknownFields != rhs.unknownFields {return false} - return true - } -} diff --git a/Meshtastic/Protobufs/meshtastic/mesh.pb.swift b/Meshtastic/Protobufs/meshtastic/mesh.pb.swift index 18da455b..1ae038f4 100644 --- a/Meshtastic/Protobufs/meshtastic/mesh.pb.swift +++ b/Meshtastic/Protobufs/meshtastic/mesh.pb.swift @@ -2619,6 +2619,35 @@ struct Heartbeat { init() {} } +/// +/// RemoteHardwarePins associated with a node +struct NodeRemoteHardwarePin { + // 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. + + /// + /// The node_num exposing the available gpio pin + var nodeNum: UInt32 = 0 + + /// + /// The the available gpio pin for usage with RemoteHardware module + var pin: RemoteHardwarePin { + get {return _pin ?? RemoteHardwarePin()} + set {_pin = newValue} + } + /// Returns true if `pin` has been explicitly set. + var hasPin: Bool {return self._pin != nil} + /// Clears the value of `pin`. Subsequent reads from it will return its default value. + mutating func clearPin() {self._pin = nil} + + var unknownFields = SwiftProtobuf.UnknownStorage() + + init() {} + + fileprivate var _pin: RemoteHardwarePin? = nil +} + #if swift(>=5.5) && canImport(_Concurrency) extension HardwareModel: @unchecked Sendable {} extension Constants: @unchecked Sendable {} @@ -2653,6 +2682,7 @@ extension NeighborInfo: @unchecked Sendable {} extension Neighbor: @unchecked Sendable {} extension DeviceMetadata: @unchecked Sendable {} extension Heartbeat: @unchecked Sendable {} +extension NodeRemoteHardwarePin: @unchecked Sendable {} #endif // swift(>=5.5) && canImport(_Concurrency) // MARK: - Code below here is support for the SwiftProtobuf runtime. @@ -4611,3 +4641,45 @@ extension Heartbeat: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementation return true } } + +extension NodeRemoteHardwarePin: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { + static let protoMessageName: String = _protobuf_package + ".NodeRemoteHardwarePin" + static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ + 1: .standard(proto: "node_num"), + 2: .same(proto: "pin"), + ] + + mutating func decodeMessage(decoder: inout D) throws { + 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.decodeSingularUInt32Field(value: &self.nodeNum) }() + case 2: try { try decoder.decodeSingularMessageField(value: &self._pin) }() + 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.nodeNum != 0 { + try visitor.visitSingularUInt32Field(value: self.nodeNum, fieldNumber: 1) + } + try { if let v = self._pin { + try visitor.visitSingularMessageField(value: v, fieldNumber: 2) + } }() + try unknownFields.traverse(visitor: &visitor) + } + + static func ==(lhs: NodeRemoteHardwarePin, rhs: NodeRemoteHardwarePin) -> Bool { + if lhs.nodeNum != rhs.nodeNum {return false} + if lhs._pin != rhs._pin {return false} + if lhs.unknownFields != rhs.unknownFields {return false} + return true + } +} diff --git a/protobufs b/protobufs index bcfb49c4..dea3a82e 160000 --- a/protobufs +++ b/protobufs @@ -1 +1 @@ -Subproject commit bcfb49c4988b1539fc35e568a58b9f2f5b60738a +Subproject commit dea3a82ef2accd25112b4ef1c6f8991b579740f4 From 08a936f7e1f321bc4e2ba52b035df032f3e5e6e0 Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Tue, 26 Mar 2024 09:13:15 -0700 Subject: [PATCH 20/34] Filter by client role too --- .../MeshtasticDataModelV 31.xcdatamodel/contents | 2 ++ Meshtastic/Views/Nodes/NodeList.swift | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/Meshtastic/Meshtastic.xcdatamodeld/MeshtasticDataModelV 31.xcdatamodel/contents b/Meshtastic/Meshtastic.xcdatamodeld/MeshtasticDataModelV 31.xcdatamodel/contents index 72ba98d4..9852b36b 100644 --- a/Meshtastic/Meshtastic.xcdatamodeld/MeshtasticDataModelV 31.xcdatamodel/contents +++ b/Meshtastic/Meshtastic.xcdatamodeld/MeshtasticDataModelV 31.xcdatamodel/contents @@ -189,6 +189,7 @@ + @@ -230,6 +231,7 @@ + diff --git a/Meshtastic/Views/Nodes/NodeList.swift b/Meshtastic/Views/Nodes/NodeList.swift index 4efad7a4..a06aa681 100644 --- a/Meshtastic/Views/Nodes/NodeList.swift +++ b/Meshtastic/Views/Nodes/NodeList.swift @@ -280,7 +280,7 @@ struct NodeList: View { } } /// Role - if deviceRole > 0 { + if deviceRole > -1 { let rolePredicate = NSPredicate(format: "user.role == %i", Int32(deviceRole)) predicates.append(rolePredicate) } From e42b1ae500794a21a4edf4d9dfd8f03856775564 Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Tue, 26 Mar 2024 09:21:47 -0700 Subject: [PATCH 21/34] force slelection of at least one option in the lora / mqtt filters --- Meshtastic/Views/Nodes/NodeList.swift | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/Meshtastic/Views/Nodes/NodeList.swift b/Meshtastic/Views/Nodes/NodeList.swift index a06aa681..0ab993c7 100644 --- a/Meshtastic/Views/Nodes/NodeList.swift +++ b/Meshtastic/Views/Nodes/NodeList.swift @@ -242,9 +242,15 @@ struct NodeList: View { searchNodeList() } .onChange(of: viaLora) { _ in + if !viaLora && !viaMqtt { + viaMqtt = true + } searchNodeList() } .onChange(of: viaMqtt) { _ in + if !viaLora && !viaMqtt { + viaLora = true + } searchNodeList() } .onChange(of: deviceRole) { _ in From 457116657afc2fd2aa2b1e6a3e6265b19e934361 Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Tue, 26 Mar 2024 09:59:07 -0700 Subject: [PATCH 22/34] Save favorite and hops away --- Meshtastic/Helpers/MeshPackets.swift | 4 ++++ Meshtastic/Persistence/UpdateCoreData.swift | 3 +++ de.lproj/Localizable.strings | 4 ++++ en.lproj/Localizable.strings | 1 + fr.lproj/Localizable.strings | 1 + he.lproj/Localizable.strings | 1 + pl.lproj/Localizable.strings | 4 ++++ zh-Hans.lproj/Localizable.strings | 4 ++++ zh-Hant-TW.lproj/Localizable.strings | 3 +++ 9 files changed, 25 insertions(+) diff --git a/Meshtastic/Helpers/MeshPackets.swift b/Meshtastic/Helpers/MeshPackets.swift index f05bb3cd..a408af14 100644 --- a/Meshtastic/Helpers/MeshPackets.swift +++ b/Meshtastic/Helpers/MeshPackets.swift @@ -263,6 +263,8 @@ func nodeInfoPacket (nodeInfo: NodeInfo, channel: UInt32, context: NSManagedObje newNode.id = Int64(nodeInfo.num) newNode.num = Int64(nodeInfo.num) newNode.channel = Int32(nodeInfo.channel) + newNode.favorite = nodeInfo.isFavorite + newNode.hopsAway = Int32(nodeInfo.hopsAway) if nodeInfo.hasDeviceMetrics { let telemetry = TelemetryEntity(context: context) @@ -346,6 +348,8 @@ func nodeInfoPacket (nodeInfo: NodeInfo, channel: UInt32, context: NSManagedObje fetchedNode[0].lastHeard = Date(timeIntervalSince1970: TimeInterval(Int64(nodeInfo.lastHeard))) fetchedNode[0].snr = nodeInfo.snr fetchedNode[0].channel = Int32(nodeInfo.channel) + fetchedNode[0].favorite = nodeInfo.isFavorite + fetchedNode[0].hopsAway = Int32(nodeInfo.hopsAway) if nodeInfo.hasUser { if (fetchedNode[0].user == nil) { diff --git a/Meshtastic/Persistence/UpdateCoreData.swift b/Meshtastic/Persistence/UpdateCoreData.swift index 98561c0d..41920890 100644 --- a/Meshtastic/Persistence/UpdateCoreData.swift +++ b/Meshtastic/Persistence/UpdateCoreData.swift @@ -144,11 +144,13 @@ func upsertNodeInfoPacket (packet: MeshPacket, context: NSManagedObjectContext) newNode.snr = packet.rxSnr newNode.rssi = packet.rxRssi newNode.viaMqtt = packet.viaMqtt + if packet.to == 4294967295 || packet.to == UserDefaults.preferredPeripheralNum { newNode.channel = Int32(packet.channel) } if let nodeInfoMessage = try? NodeInfo(serializedData: packet.decoded.payload) { newNode.hopsAway = Int32(nodeInfoMessage.hopsAway) + newNode.favorite = nodeInfoMessage.isFavorite } else if packet.hopStart != 0 && packet.hopLimit <= packet.hopStart { newNode.hopsAway = Int32(packet.hopStart - packet.hopLimit) } @@ -222,6 +224,7 @@ func upsertNodeInfoPacket (packet: MeshPacket, context: NSManagedObjectContext) if let nodeInfoMessage = try? NodeInfo(serializedData: packet.decoded.payload) { fetchedNode[0].hopsAway = Int32(nodeInfoMessage.hopsAway) + fetchedNode[0].favorite = nodeInfoMessage.isFavorite if nodeInfoMessage.hasDeviceMetrics { let telemetry = TelemetryEntity(context: context) telemetry.batteryLevel = Int32(nodeInfoMessage.deviceMetrics.batteryLevel) diff --git a/de.lproj/Localizable.strings b/de.lproj/Localizable.strings index 2599d7b2..c3d3d0fc 100644 --- a/de.lproj/Localizable.strings +++ b/de.lproj/Localizable.strings @@ -98,6 +98,10 @@ "device.role.routerclient"="Router Client - Mesh Pakete werden bevorzugt über diesen Node gerouted. Der Router Client kann parallel auch von einer Client-App genutzt werden."; "device.role.repeater"="Repeater - Mesh packets will prefer to be routed over this node. This role eliminates unnecessary overhead such as NodeInfo, DeviceTelemetry, and any other mesh packet, resulting in the device not appearing as part of the network. Please see Rebroadcast Mode for additional settings specific to this role."; "device.role.tracker"="Tracker - For use with devices intended as a GPS tracker. Position packets sent from this device will be higher priority, with position broadcasting every two minutes. Smart Position Broadcast will default to off."; +"device.role.lostandfound"="Broadcasts location as message to default channel regularly for to assist with device recovery."; +"device.role.sensor"="Broadcasts telemetry packets as priority."; +"device.role.tak"="Optimized for ATAK system communication, reduces routine broadcasts."; +"device.role.taktracker"="Enables automatic TAK PLI broadcasts and reduces routine broadcasts."; "direct.messages"="Direktnachrichten"; "dismiss.keyboard"="Dismiss Keyboard"; "display"="Display (Device Screen)"; diff --git a/en.lproj/Localizable.strings b/en.lproj/Localizable.strings index 251e1b0d..e0d113b8 100644 --- a/en.lproj/Localizable.strings +++ b/en.lproj/Localizable.strings @@ -99,6 +99,7 @@ "device.role.lostandfound"="Broadcasts location as message to default channel regularly for to assist with device recovery."; "device.role.sensor"="Broadcasts telemetry packets as priority."; "device.role.tak"="Optimized for ATAK system communication, reduces routine broadcasts."; +"device.role.taktracker"="Enables automatic TAK PLI broadcasts and reduces routine broadcasts."; "device.role.repeater"="Infrastructure node for extending network coverage by relaying messages with minimal overhead. Not visible in Nodes list."; "device.role.router"="Infrastructure node for extending network coverage by relaying messages. Visible in Nodes list."; "device.role.routerclient"="Combination of both ROUTER and CLIENT. Not for mobile devices."; diff --git a/fr.lproj/Localizable.strings b/fr.lproj/Localizable.strings index 31642beb..b41a35a4 100644 --- a/fr.lproj/Localizable.strings +++ b/fr.lproj/Localizable.strings @@ -76,6 +76,7 @@ "device.role.lostandfound"="Transmet régulièrement la position par message dans le canal par défaut pour vous aider à retrouver l'appareil."; "device.role.sensor"="Transmet les paquets de télémétrie en priorité."; "device.role.tak"="Optimisé pour le système de communication ATAK, diminue les émissions de routine."; +"device.role.taktracker"="Enables automatic TAK PLI broadcasts and reduces routine broadcasts."; "device.role.repeater"="Noeud d'infrastructure qui étend la couverture du réseau en relayant les messages avec un minimum de surcharge. Invisible dans la liste des noeuds."; "device.role.router"="Noeud d'infrastructure qui étend la couverture du réseau en relayant les messages. Visible dans la liste des noeuds."; "device.role.routerclient"="Combinaison des modes ROUTER et CLIENT. Pas pour les appareils mobiles."; diff --git a/he.lproj/Localizable.strings b/he.lproj/Localizable.strings index 22e5a2df..e9dc8ce1 100644 --- a/he.lproj/Localizable.strings +++ b/he.lproj/Localizable.strings @@ -99,6 +99,7 @@ "device.role.lostandfound"="משדר מיקום כהודעה לערוץ ברירת מחדל לעיתים קבועות בכדי לסייע במציאת המכשיר."; "device.role.sensor"="משדר טלמטריה בעדיפות גבוהה."; "device.role.tak"="מותאם למערכת ATAK, מקטין תקשורת קבועה."; +"device.role.taktracker"="Enables automatic TAK PLI broadcasts and reduces routine broadcasts."; "device.role.repeater"="מכשיר תשתית להרחבת המש על ידי העברת הודעות עם דאטה נוסף מינימלי."; "device.role.router"="מכשיר תשתית להרחבת המש על ידי העברת הודעות. מופיע ברשימת מכשירים."; "device.role.routerclient"="קומבינציה של ROUTER וCLIENT. לא למכשירים ניידים."; diff --git a/pl.lproj/Localizable.strings b/pl.lproj/Localizable.strings index 2c5aa853..5e194834 100644 --- a/pl.lproj/Localizable.strings +++ b/pl.lproj/Localizable.strings @@ -100,6 +100,10 @@ "device.role.routerclient"="Router Client - Hybryda ról klienta i routera. Podobnie jak w przypadku routera, z tym że Router Client może być używany zarówno jako router, jak i klient połączony z aplikacją. Radia BLE/Wi-Fi i ekran OLED nie zostaną uśpione."; "device.role.repeater"="Przekaźnik - Pakiety siatki będą preferować trasowanie przez ten węzeł. Ta rola eliminuje niepotrzebny nadmiar, taki jak NodeInfo, DeviceTelemetry i inne pakiety siatki, skutkując tym, że urządzenie nie będzie widoczne jako część sieci. Proszę zobaczyć tryb Rebroadcast dla dodatkowych ustawień specyficznych dla tej roli."; "device.role.tracker"="Tracker - Do użytku z urządzeniami przeznaczonymi jako śledzenie GPS. Pakiety pozycyjne wysyłane z tego urządzenia będą miały wyższy priorytet, z nadawaniem pozycji co dwie minuty. Inteligentna transmisja pozycji będzie domyślnie wyłączona."; +"device.role.lostandfound"="Broadcasts location as message to default channel regularly for to assist with device recovery."; +"device.role.sensor"="Broadcasts telemetry packets as priority."; +"device.role.tak"="Optimized for ATAK system communication, reduces routine broadcasts."; +"device.role.taktracker"="Enables automatic TAK PLI broadcasts and reduces routine broadcasts."; "direct.messages"="Bezpośrednie Wiadomości"; "dismiss.keyboard"="Zamknij"; "display"="Wyświetlacz (Ekran Urządzenia)"; diff --git a/zh-Hans.lproj/Localizable.strings b/zh-Hans.lproj/Localizable.strings index 6c8f6b0a..d4521ef2 100644 --- a/zh-Hans.lproj/Localizable.strings +++ b/zh-Hans.lproj/Localizable.strings @@ -98,6 +98,10 @@ "device.role.routerclient"="路由客户端模式 - 优先转发 Mesh 网络中其他节点的消息,App 也可以连接到电台进行收发操作。"; "device.role.repeater"="中继模式 - Mesh 网络数据包将优先通过此节点路由。此模式可消除不必要的开销,如 NodeInfo、DeviceTelemetry 和任何其他 Mesh 数据包,从而使设备不显示为 Mesh 网络的一部分。有关此角色的其他特定设置,请参阅转播模式。"; "device.role.tracker"="定位模式 - 用于作为 GPS 跟踪器。从该设备发送的定位数据包优先级较高,每两分钟广播一次。智能位置广播默认为关闭。"; +"device.role.lostandfound"="Broadcasts location as message to default channel regularly for to assist with device recovery."; +"device.role.sensor"="Broadcasts telemetry packets as priority."; +"device.role.tak"="Optimized for ATAK system communication, reduces routine broadcasts."; +"device.role.taktracker"="Enables automatic TAK PLI broadcasts and reduces routine broadcasts."; "direct.messages"="直频消息"; "dismiss.keyboard"="隐藏键盘"; "display"="屏幕(电台屏幕)"; diff --git a/zh-Hant-TW.lproj/Localizable.strings b/zh-Hant-TW.lproj/Localizable.strings index d0f8e876..8bef85fd 100644 --- a/zh-Hant-TW.lproj/Localizable.strings +++ b/zh-Hant-TW.lproj/Localizable.strings @@ -98,6 +98,9 @@ "device.role.routerclient"="路由客户端模式 - 優先轉發 Mesh 網路中其他中繼點的消息,App 也可以連接到電台進行收發操作。"; "device.role.repeater"="中繼模式 - Mesh 網路數據包將優先通過此中繼點路由。此模式可消除不必要的開銷,如 NodeInfo、DeviceTelemetry 和任何其他 Mesh 數據包,從而使設備不顯示為 Mesh 網路的一部分。有關此角色的其他特定設置,請參閱轉播模式。"; "device.role.tracker"="追蹤模式 - 用於作為 GPS 追蹤器。從該設備發送的定位數據包優先級較高,每兩分鐘廣播一次。智能位置廣播預設為關閉。"; +"device.role.sensor"="Broadcasts telemetry packets as priority."; +"device.role.tak"="Optimized for ATAK system communication, reduces routine broadcasts."; +"device.role.taktracker"="Enables automatic TAK PLI broadcasts and reduces routine broadcasts."; "direct.messages"="聊天"; "dismiss.keyboard"="隱藏鍵盤"; "display"="螢幕(電台螢幕)"; From 5cd2b3342b330aae8f9ad71b8331a0390befaa61 Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Tue, 26 Mar 2024 13:26:23 -0700 Subject: [PATCH 23/34] Mqtt config updates --- .../contents | 2 + Meshtastic/Persistence/UpdateCoreData.swift | 6 + .../Settings/Config/Module/MQTTConfig.swift | 200 ++++++++++++------ 3 files changed, 147 insertions(+), 61 deletions(-) diff --git a/Meshtastic/Meshtastic.xcdatamodeld/MeshtasticDataModelV 31.xcdatamodel/contents b/Meshtastic/Meshtastic.xcdatamodeld/MeshtasticDataModelV 31.xcdatamodel/contents index 9852b36b..16c3d91a 100644 --- a/Meshtastic/Meshtastic.xcdatamodeld/MeshtasticDataModelV 31.xcdatamodel/contents +++ b/Meshtastic/Meshtastic.xcdatamodeld/MeshtasticDataModelV 31.xcdatamodel/contents @@ -189,6 +189,8 @@ + + diff --git a/Meshtastic/Persistence/UpdateCoreData.swift b/Meshtastic/Persistence/UpdateCoreData.swift index 41920890..f2933922 100644 --- a/Meshtastic/Persistence/UpdateCoreData.swift +++ b/Meshtastic/Persistence/UpdateCoreData.swift @@ -1119,6 +1119,9 @@ func upsertMqttModuleConfigPacket(config: Meshtastic.ModuleConfig.MQTTConfig, no newMQTTConfig.encryptionEnabled = config.encryptionEnabled newMQTTConfig.jsonEnabled = config.jsonEnabled newMQTTConfig.tlsEnabled = config.tlsEnabled + newMQTTConfig.mapReportingEnabled = config.mapReportingEnabled + newMQTTConfig.mapPositionPrecision = Int32(config.mapReportSettings.positionPrecision) + newMQTTConfig.mapPublishIntervalSecs = Int32(config.mapReportSettings.publishIntervalSecs) fetchedNode[0].mqttConfig = newMQTTConfig } else { fetchedNode[0].mqttConfig?.enabled = config.enabled @@ -1130,6 +1133,9 @@ func upsertMqttModuleConfigPacket(config: Meshtastic.ModuleConfig.MQTTConfig, no fetchedNode[0].mqttConfig?.encryptionEnabled = config.encryptionEnabled fetchedNode[0].mqttConfig?.jsonEnabled = config.jsonEnabled fetchedNode[0].mqttConfig?.tlsEnabled = config.tlsEnabled + fetchedNode[0].mqttConfig?.mapReportingEnabled = config.mapReportingEnabled + fetchedNode[0].mqttConfig?.mapPositionPrecision = Int32(config.mapReportSettings.positionPrecision) + fetchedNode[0].mqttConfig?.mapPublishIntervalSecs = Int32(config.mapReportSettings.publishIntervalSecs) } do { try context.save() diff --git a/Meshtastic/Views/Settings/Config/Module/MQTTConfig.swift b/Meshtastic/Views/Settings/Config/Module/MQTTConfig.swift index 8191aec9..10563eb3 100644 --- a/Meshtastic/Views/Settings/Config/Module/MQTTConfig.swift +++ b/Meshtastic/Views/Settings/Config/Module/MQTTConfig.swift @@ -28,6 +28,12 @@ struct MQTTConfig: View { @State var mqttConnected: Bool = false @State var defaultTopic = "msh/US" @State var nearbyTopics = [String]() + @State var mapReportingEnabled = false + @State var mapPublishIntervalSecs = 3600 + @State var preciseLocation: Bool = false + @State var mapPositionPrecision: Double = 13.0 + + let locale = Locale.current var body: some View { @@ -58,7 +64,7 @@ struct MQTTConfig: View { } .toggleStyle(SwitchToggleStyle(tint: .accentColor)) - if enabled && proxyToClientEnabled { + if enabled && proxyToClientEnabled && node!.mqttConfig!.proxyToClientEnabled == true { Toggle(isOn: $mqttConnected) { Label(mqttConnected ? "mqtt.disconnect".localized : "mqtt.connect".localized, systemImage: "server.rack") } @@ -75,14 +81,95 @@ struct MQTTConfig: View { Text("JSON mode is a limited, unencrypted MQTT output for locally integrating with home assistant") } .toggleStyle(SwitchToggleStyle(tint: .accentColor)) + } + + Section(header: Text("Map Report")) { - Toggle(isOn: $tlsEnabled) { - Label("TLS Enabled", systemImage: "checkmark.shield.fill") - Text("Your MQTT Server must support TLS.") + Toggle(isOn: $mapReportingEnabled) { + Label("enabled", systemImage: "map") } .toggleStyle(SwitchToggleStyle(tint: .accentColor)) - + if mapReportingEnabled { + Picker("Map Publish Interval", selection: $mapPublishIntervalSecs ) { + ForEach(UpdateIntervals.allCases) { ui in + if ui.rawValue >= 3600 { + Text(ui.description) + } + } + } + .pickerStyle(DefaultPickerStyle()) + + VStack(alignment: .leading) { + Toggle(isOn: $preciseLocation) { + Label("Precise Location", systemImage: "scope") + } + .toggleStyle(SwitchToggleStyle(tint: .accentColor)) + .listRowSeparator(.visible) + .onChange(of: preciseLocation) { pl in + if pl == false { + mapPositionPrecision = 12 + } else { + mapPositionPrecision = 32 + } + } + } + + if !preciseLocation { + VStack(alignment: .leading) { + Label("Approximate Location", systemImage: "location.slash.circle.fill") + Slider(value: $mapPositionPrecision, in: 11...16, step: 1) { + } minimumValueLabel: { + Image(systemName: "minus") + } maximumValueLabel: { + Image(systemName: "plus") + } + Text(PositionPrecision(rawValue: Int(mapPositionPrecision))?.description ?? "") + .foregroundColor(.gray) + .font(.callout) + } + } + } } + Section(header: Text("Root Topic")) { + HStack { + Label("Root Topic", systemImage: "tree") + TextField("Root Topic", text: $root) + .foregroundColor(.gray) + .onChange(of: root, perform: { _ in + let totalBytes = root.utf8.count + // Only mess with the value if it is too big + if totalBytes > 30 { + let firstNBytes = Data(root.utf8.prefix(30)) + if let maxBytesString = String(data: firstNBytes, encoding: String.Encoding.utf8) { + // Set the shortName back to the last place where it was the right size + root = maxBytesString + } + } + }) + .foregroundColor(.gray) + } + .keyboardType(.asciiCapable) + .scrollDismissesKeyboard(.interactively) + .disableAutocorrection(true) + .listRowSeparator(.hidden) + Text("The root topic to use for MQTT.") + .foregroundColor(.gray) + .font(.callout) + + if nearbyTopics.count > 0 { + Picker("Nearby Topics", selection: $selectedTopic ) { + ForEach(nearbyTopics, id: \.self) { nt in + Text(nt) + } + } + .pickerStyle(InlinePickerStyle()) + .listRowSeparator(.hidden) + Text("If the default region topic is too busy you can choose a more local topic.") + .foregroundColor(.gray) + .font(.callout) + } + } + Section(header: Text("Server")) { HStack { Label("Address", systemImage: "server.rack") @@ -161,45 +248,13 @@ struct MQTTConfig: View { .keyboardType(.default) .scrollDismissesKeyboard(.interactively) .listRowSeparator(/*@START_MENU_TOKEN@*/.visible/*@END_MENU_TOKEN@*/) - HStack { - Label("Root Topic", systemImage: "tree") - TextField("Root Topic", text: $root) - .foregroundColor(.gray) - .onChange(of: root, perform: { _ in - let totalBytes = root.utf8.count - // Only mess with the value if it is too big - if totalBytes > 30 { - let firstNBytes = Data(root.utf8.prefix(30)) - if let maxBytesString = String(data: firstNBytes, encoding: String.Encoding.utf8) { - // Set the shortName back to the last place where it was the right size - root = maxBytesString - } - } - }) - .foregroundColor(.gray) - } - .keyboardType(.asciiCapable) - .scrollDismissesKeyboard(.interactively) - .disableAutocorrection(true) - .listRowSeparator(.hidden) - Text("The root topic to use for MQTT.") - .foregroundColor(.gray) - .font(.callout) - - if nearbyTopics.count > 0 { - Picker("Nearby Topics", selection: $selectedTopic ) { - ForEach(nearbyTopics, id: \.self) { nt in - Text(nt) - } - } - .pickerStyle(InlinePickerStyle()) - .listRowSeparator(.hidden) - Text("If the default region topic is too busy you can choose a more local topic.") - .foregroundColor(.gray) - .font(.callout) + Toggle(isOn: $tlsEnabled) { + Label("TLS Enabled", systemImage: "checkmark.shield.fill") + Text("Your MQTT Server must support TLS.") } + .toggleStyle(SwitchToggleStyle(tint: .accentColor)) } - Text("You can set uplink and downlink for each channel.") + Text("For all Mqtt functionality other than the map report you must also set uplink and downlink for each channel you want to brige over Mqtt.") .font(.callout) } .scrollDismissesKeyboard(.interactively) @@ -219,6 +274,9 @@ struct MQTTConfig: View { mqtt.encryptionEnabled = self.encryptionEnabled mqtt.jsonEnabled = self.jsonEnabled mqtt.tlsEnabled = self.tlsEnabled + mqtt.mapReportingEnabled = self.mapReportingEnabled + mqtt.mapReportSettings.positionPrecision = UInt32(self.mapPositionPrecision) + mqtt.mapReportSettings.publishIntervalSecs = UInt32(self.mapPublishIntervalSecs) let adminMessageId = bleManager.saveMQTTConfig(config: mqtt, fromUser: connectedNode!.user!, toUser: node!.user!, adminIndex: connectedNode?.myInfo?.adminIndex ?? 0) if adminMessageId > 0 { // Should show a saved successfully alert once I know that to be true @@ -233,20 +291,6 @@ struct MQTTConfig: View { ZStack { ConnectedDevice(bluetoothOn: bleManager.isSwitchedOn, deviceConnected: bleManager.connectedPeripheral != nil, name: (bleManager.connectedPeripheral != nil) ? bleManager.connectedPeripheral.shortName : "?", mqttProxyConnected: bleManager.mqttProxyConnected) }) - .onAppear { - if self.bleManager.context == nil { - self.bleManager.context = context - } - setMqttValues() - // Need to request a TelemetryModuleConfig from the remote node before allowing changes - if bleManager.connectedPeripheral != nil && node?.mqttConfig == nil { - print("empty mqtt module config") - let connectedNode = getNodeInfo(id: bleManager.connectedPeripheral.num, context: context) - if node != nil && connectedNode != nil { - _ = bleManager.requestMqttModuleConfig(fromUser: connectedNode!.user!, toUser: node!.user!, adminIndex: connectedNode?.myInfo?.adminIndex ?? 0) - } - } - } .onChange(of: address) { newAddress in if node != nil && node?.mqttConfig != nil { if newAddress != node!.mqttConfig!.address { hasChanges = true } @@ -315,6 +359,33 @@ struct MQTTConfig: View { } } } + .onChange(of: mapReportingEnabled) { newMapReportingEnabled in + if node != nil && node?.mqttConfig != nil { + if newMapReportingEnabled != node!.mqttConfig!.mapReportingEnabled { hasChanges = true } + } + } + .onChange(of: preciseLocation) { _ in + hasChanges = true + } + .onChange(of: mapPublishIntervalSecs) { newMapPublishIntervalSecs in + if node != nil && node?.mqttConfig != nil { + if newMapPublishIntervalSecs != node!.mqttConfig!.mapPublishIntervalSecs { hasChanges = true } + } + } + .onAppear { + if self.bleManager.context == nil { + self.bleManager.context = context + } + setMqttValues() + // Need to request a TelemetryModuleConfig from the remote node before allowing changes + if bleManager.connectedPeripheral != nil && node?.mqttConfig == nil { + print("empty mqtt module config") + let connectedNode = getNodeInfo(id: bleManager.connectedPeripheral.num, context: context) + if node != nil && connectedNode != nil { + _ = bleManager.requestMqttModuleConfig(fromUser: connectedNode!.user!, toUser: node!.user!, adminIndex: connectedNode?.myInfo?.adminIndex ?? 0) + } + } + } } func setMqttValues() { @@ -367,16 +438,23 @@ struct MQTTConfig: View { }) } } - self.enabled = (node?.mqttConfig?.enabled ?? false) - self.proxyToClientEnabled = (node?.mqttConfig?.proxyToClientEnabled ?? false) + self.enabled = node?.mqttConfig?.enabled ?? false + self.proxyToClientEnabled = node?.mqttConfig?.proxyToClientEnabled ?? false self.address = node?.mqttConfig?.address ?? "" self.username = node?.mqttConfig?.username ?? "" self.password = node?.mqttConfig?.password ?? "" self.root = node?.mqttConfig?.root ?? "msh" - self.encryptionEnabled = (node?.mqttConfig?.encryptionEnabled ?? false) - self.jsonEnabled = (node?.mqttConfig?.jsonEnabled ?? false) - self.tlsEnabled = (node?.mqttConfig?.tlsEnabled ?? false) + self.encryptionEnabled = node?.mqttConfig?.encryptionEnabled ?? false + self.jsonEnabled = node?.mqttConfig?.jsonEnabled ?? false + self.tlsEnabled = node?.mqttConfig?.tlsEnabled ?? false self.mqttConnected = bleManager.mqttProxyConnected + self.mapReportingEnabled = node?.mqttConfig?.mapReportingEnabled ?? false + self.mapPublishIntervalSecs = Int(node?.mqttConfig?.mapPublishIntervalSecs ?? 3600) + self.mapPositionPrecision = Double(node?.mqttConfig?.mapPositionPrecision ?? 12) + if mapPositionPrecision == 0.0 { + self.mapPositionPrecision = 12 + } + self.preciseLocation = mapPositionPrecision == 32 self.hasChanges = false } } From 35c943aa68b1be10ff6fbbc0410983c8a44ae0ca Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Tue, 26 Mar 2024 14:01:59 -0700 Subject: [PATCH 24/34] Fix text search not running if there was not a filter set, fix typo on pax counter settings localized string --- Meshtastic/Views/Nodes/NodeList.swift | 2 +- Meshtastic/Views/Settings/Settings.swift | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Meshtastic/Views/Nodes/NodeList.swift b/Meshtastic/Views/Nodes/NodeList.swift index 0ab993c7..2e8bda18 100644 --- a/Meshtastic/Views/Nodes/NodeList.swift +++ b/Meshtastic/Views/Nodes/NodeList.swift @@ -316,7 +316,7 @@ struct NodeList: View { } } - if predicates.count > 0 { + if predicates.count > 0 || !searchText.isEmpty { if !searchText.isEmpty { let filterPredicates = NSCompoundPredicate(type: .and, subpredicates: predicates) diff --git a/Meshtastic/Views/Settings/Settings.swift b/Meshtastic/Views/Settings/Settings.swift index b1883c1c..f0b221aa 100644 --- a/Meshtastic/Views/Settings/Settings.swift +++ b/Meshtastic/Views/Settings/Settings.swift @@ -333,7 +333,7 @@ struct Settings: View { PaxCounterConfig(node: nodes.first(where: { $0.num == selectedNode })) } label: { Label { - Text("config.module.paxcounter.setting") + Text("config.module.paxcounter.settings") } icon: { Image(systemName: "figure.walk.motion") } From 0c112e2a5571b52f7dbec124761c966c1ac641b3 Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Tue, 26 Mar 2024 14:57:00 -0700 Subject: [PATCH 25/34] Online only filter --- Meshtastic/Views/Nodes/Helpers/NodeListFilter.swift | 13 +++++++++++++ Meshtastic/Views/Nodes/NodeList.swift | 12 +++++++++++- .../Views/Settings/Config/Module/MQTTConfig.swift | 2 +- 3 files changed, 25 insertions(+), 2 deletions(-) diff --git a/Meshtastic/Views/Nodes/Helpers/NodeListFilter.swift b/Meshtastic/Views/Nodes/Helpers/NodeListFilter.swift index 03e04d84..a49fa9f2 100644 --- a/Meshtastic/Views/Nodes/Helpers/NodeListFilter.swift +++ b/Meshtastic/Views/Nodes/Helpers/NodeListFilter.swift @@ -13,6 +13,7 @@ struct NodeListFilter: View { /// Filters @Binding var viaLora: Bool @Binding var viaMqtt: Bool + @Binding var isOnline: Bool @Binding var distanceFilter: Bool @Binding var maximumDistance: Double @Binding var hopsAway: Int @@ -44,6 +45,18 @@ struct NodeListFilter: View { .toggleStyle(SwitchToggleStyle(tint: .accentColor)) .listRowSeparator(.visible) + Toggle(isOn: $isOnline) { + + Label { + Text("Online Only") + } icon: { + Image(systemName: "checkmark.circle.fill") + .foregroundColor(.green) + } + } + .toggleStyle(SwitchToggleStyle(tint: .accentColor)) + .listRowSeparator(.visible) + // Toggle(isOn: $distanceFilter) { // // Label { diff --git a/Meshtastic/Views/Nodes/NodeList.swift b/Meshtastic/Views/Nodes/NodeList.swift index 2e8bda18..a750658b 100644 --- a/Meshtastic/Views/Nodes/NodeList.swift +++ b/Meshtastic/Views/Nodes/NodeList.swift @@ -19,6 +19,7 @@ struct NodeList: View { @State private var searchText = "" @State private var viaLora = true @State private var viaMqtt = true + @State private var isOnline = false @State private var distanceFilter = false @State private var maxDistance: Double = 800000 @State private var hopsAway: Int = -1 @@ -144,7 +145,7 @@ struct NodeList: View { } } .sheet(isPresented: $isEditingFilters) { - NodeListFilter(viaLora: $viaLora, viaMqtt: $viaMqtt, distanceFilter: $distanceFilter, maximumDistance: $maxDistance, hopsAway: $hopsAway, deviceRole: $deviceRole) + NodeListFilter(viaLora: $viaLora, viaMqtt: $viaMqtt, isOnline: $isOnline, distanceFilter: $distanceFilter, maximumDistance: $maxDistance, hopsAway: $hopsAway, deviceRole: $deviceRole) } .safeAreaInset(edge: .bottom, alignment: .trailing) { HStack { @@ -259,6 +260,9 @@ struct NodeList: View { .onChange(of: hopsAway) { _ in searchNodeList() } + .onChange(of: isOnline) { _ in + searchNodeList() + } .onAppear { if self.bleManager.context == nil { self.bleManager.context = context @@ -295,6 +299,12 @@ struct NodeList: View { let hopsAwayPredicate = NSPredicate(format: "hopsAway == %i", Int32(hopsAway)) predicates.append(hopsAwayPredicate) } + + /// Online + if isOnline { + let isOnlinePredicate = NSPredicate(format: "lastHeard >= %@", Calendar.current.date(byAdding: .minute, value: -15, to: Date())! as NSDate) + predicates.append(isOnlinePredicate) + } /// Distance if distanceFilter { let pointOfInterest = LocationHelper.currentLocation diff --git a/Meshtastic/Views/Settings/Config/Module/MQTTConfig.swift b/Meshtastic/Views/Settings/Config/Module/MQTTConfig.swift index 10563eb3..be067293 100644 --- a/Meshtastic/Views/Settings/Config/Module/MQTTConfig.swift +++ b/Meshtastic/Views/Settings/Config/Module/MQTTConfig.swift @@ -254,7 +254,7 @@ struct MQTTConfig: View { } .toggleStyle(SwitchToggleStyle(tint: .accentColor)) } - Text("For all Mqtt functionality other than the map report you must also set uplink and downlink for each channel you want to brige over Mqtt.") + Text("For all Mqtt functionality other than the map report you must also set uplink and downlink for each channel you want to bridge over Mqtt.") .font(.callout) } .scrollDismissesKeyboard(.interactively) From 1354427bfe2d681b8267a87a705157626746b101 Mon Sep 17 00:00:00 2001 From: ChDel Date: Tue, 26 Mar 2024 16:45:32 -0700 Subject: [PATCH 26/34] Fixing UserDefault wrapper crashing with enums --- Meshtastic/Enums/AppSettingsEnums.swift | 6 +- Meshtastic/Extensions/UserDefaults.swift | 447 +++++------------- .../Config/Module/DetectionSensorConfig.swift | 2 +- 3 files changed, 115 insertions(+), 340 deletions(-) diff --git a/Meshtastic/Enums/AppSettingsEnums.swift b/Meshtastic/Enums/AppSettingsEnums.swift index 0250119a..c53c3826 100644 --- a/Meshtastic/Enums/AppSettingsEnums.swift +++ b/Meshtastic/Enums/AppSettingsEnums.swift @@ -130,7 +130,7 @@ enum LocationUpdateInterval: Int, CaseIterable, Identifiable { } } -enum MapLayer: String, CaseIterable, Equatable { +enum MapLayer: String, CaseIterable, Equatable, Decodable { case standard case hybrid case satellite @@ -138,7 +138,7 @@ enum MapLayer: String, CaseIterable, Equatable { var localized: String { self.rawValue.localized } } -enum MapTileServer: String, CaseIterable, Identifiable { +enum MapTileServer: String, CaseIterable, Identifiable, Decodable { case openStreetMap case openStreetMapDE case openStreetMapFR @@ -273,7 +273,7 @@ enum OverlayType: String, CaseIterable, Equatable { var localized: String { self.rawValue.localized } } -enum MapOverlayServer: String, CaseIterable, Identifiable { +enum MapOverlayServer: String, CaseIterable, Identifiable, Decodable { case baseReReflectivityCurrent case baseReReflectivityOneHourAgo case echoTopsEetCurrent diff --git a/Meshtastic/Extensions/UserDefaults.swift b/Meshtastic/Extensions/UserDefaults.swift index ccb8a7a3..c714efbf 100644 --- a/Meshtastic/Extensions/UserDefaults.swift +++ b/Meshtastic/Extensions/UserDefaults.swift @@ -13,6 +13,36 @@ import Foundation +@propertyWrapper +struct UserDefault { + let key: UserDefaults.Keys + let defaultValue: T + + init(_ key: UserDefaults.Keys, defaultValue: T) { + self.key = key + self.defaultValue = defaultValue + } + + var wrappedValue: T { + get { + if defaultValue as? any RawRepresentable != nil { + let storedValue = UserDefaults.standard.object(forKey: key.rawValue) + + guard let storedValue, + let data = ("\"\(storedValue)\"".data(using: .utf8)), + let value = (try? JSONDecoder().decode(T.self, from: data)) else { return defaultValue } + + return value + } + + return UserDefaults.standard.object(forKey: key.rawValue) as? T ?? defaultValue + } + set { + UserDefaults.standard.set((newValue as? any RawRepresentable)?.rawValue ?? newValue, forKey: key.rawValue) + } + } +} + extension UserDefaults { enum Keys: String, CaseIterable { case preferredPeripheralId @@ -26,10 +56,16 @@ extension UserDefaults { case meshMapShowNodeHistory case meshMapShowRouteLines case enableMapConvexHull + case enableMapRecentering + case enableMapNodeHistoryPins + case enableMapRouteLines case enableMapTraffic case enableMapPointsOfInterest case enableOfflineMaps + case enableOfflineMapsMBTiles case mapTileServer + case enableOverlayServer + case mapOverlayServer case mapTilesAboveLabels case mapUseLegacy case enableDetectionNotifications @@ -42,340 +78,79 @@ extension UserDefaults { func reset() { Keys.allCases.forEach { removeObject(forKey: $0.rawValue) } } - static var preferredPeripheralId: String { - get { - UserDefaults.standard.string(forKey: "preferredPeripheralId") ?? "" - } - set { - UserDefaults.standard.set(newValue, forKey: "preferredPeripheralId") - } - } - static var preferredPeripheralNum: Int { - get { - UserDefaults.standard.integer(forKey: "preferredPeripheralNum") - } - set { - UserDefaults.standard.set(newValue, forKey: "preferredPeripheralNum") - } - } - static var provideLocation: Bool { - get { - UserDefaults.standard.bool(forKey: "provideLocation") - } set { - UserDefaults.standard.set(newValue, forKey: "provideLocation") - } - } - static var provideLocationInterval: Int { - get { - UserDefaults.standard.integer(forKey: "provideLocationInterval") - } - set { - UserDefaults.standard.set(newValue, forKey: "provideLocationInterval") - } - } - static var mapLayer: MapLayer { - get { - MapLayer(rawValue: UserDefaults.standard.string(forKey: "mapLayer") ?? MapLayer.standard.rawValue) ?? MapLayer.standard - } - set { - UserDefaults.standard.set(newValue.rawValue, forKey: "mapLayer") - } - } - static var meshMapDistance: Double { - get { - UserDefaults.standard.double(forKey: "meshMapDistance") - } - set { - UserDefaults.standard.set(newValue, forKey: "meshMapDistance") - } - } - static var enableMapWaypoints: Bool { - get { - UserDefaults.standard.bool(forKey: "enableMapWaypoints") - } - set { - UserDefaults.standard.set(newValue, forKey: "enableMapWaypoints") - } - } - static var enableMapRecentering: Bool { - get { - UserDefaults.standard.bool(forKey: "meshMapRecentering") - } - set { - UserDefaults.standard.set(newValue, forKey: "meshMapRecentering") - } - } - static var enableMapNodeHistoryPins: Bool { - get { - UserDefaults.standard.bool(forKey: "meshMapShowNodeHistory") - } - set { - UserDefaults.standard.set(newValue, forKey: "meshMapShowNodeHistory") - } - } - static var enableMapRouteLines: Bool { - get { - UserDefaults.standard.bool(forKey: "meshMapShowRouteLines") - } - set { - UserDefaults.standard.set(newValue, forKey: "meshMapShowRouteLines") - } - } - static var enableMapConvexHull: Bool { - get { - UserDefaults.standard.bool(forKey: "enableMapConvexHull") - } - set { - UserDefaults.standard.set(newValue, forKey: "enableMapConvexHull") - } - } - static var enableMapTraffic: Bool { - get { - UserDefaults.standard.bool(forKey: "enableMapTraffic") - } - set { - UserDefaults.standard.set(newValue, forKey: "enableMapTraffic") - } - } - static var enableMapPointsOfInterest: Bool { - get { - UserDefaults.standard.bool(forKey: "enableMapPointsOfInterest") - } - set { - UserDefaults.standard.set(newValue, forKey: "enableMapPointsOfInterest") - } - } - static var enableOfflineMaps: Bool { - get { - UserDefaults.standard.bool(forKey: "enableOfflineMaps") - } - set { - UserDefaults.standard.set(newValue, forKey: "enableOfflineMaps") - } - } - static var enableOfflineMapsMBTiles: Bool { - get { - UserDefaults.standard.bool(forKey: "enableOfflineMapsMBTiles") - } - set { - UserDefaults.standard.set(newValue, forKey: "enableOfflineMapsMBTiles") - } - } - static var mapTileServer: MapTileServer { - get { - MapTileServer(rawValue: UserDefaults.standard.string(forKey: "mapTileServer") ?? MapTileServer.openStreetMap.rawValue) ?? MapTileServer.openStreetMap - } - set { - UserDefaults.standard.set(newValue.rawValue, forKey: "mapTileServer") - } - } - static var enableOverlayServer: Bool { - get { - UserDefaults.standard.bool(forKey: "enableOverlayServer") - } - set { - UserDefaults.standard.set(newValue, forKey: "enableOverlayServer") - } - } - static var mapOverlayServer: MapOverlayServer { - get { - MapOverlayServer(rawValue: UserDefaults.standard.string(forKey: "mapOverlayServer") ?? MapOverlayServer.baseReReflectivityCurrent.rawValue) ?? MapOverlayServer.baseReReflectivityCurrent - } - set { - UserDefaults.standard.set(newValue.rawValue, forKey: "mapOverlayServer") - } - } - static var mapTilesAboveLabels: Bool { - get { - UserDefaults.standard.bool(forKey: "mapTilesAboveLabels") - } - set { - UserDefaults.standard.set(newValue, forKey: "mapTilesAboveLabels") - } - } - - static var mapUseLegacy: Bool { - get { - UserDefaults.standard.bool(forKey: "mapUseLegacy") - } - set { - UserDefaults.standard.set(newValue, forKey: "mapUseLegacy") - } - } - - static var enableDetectionNotifications: Bool { - get { - UserDefaults.standard.bool(forKey: "enableDetectionNotifications") - } - set { - UserDefaults.standard.set(newValue, forKey: "enableDetectionNotifications") - } - } - - static var detectionSensorRole: DetectionSensorRole { - get { - DetectionSensorRole(rawValue: UserDefaults.standard.string(forKey: "detectionSensorRole") ?? DetectionSensorRole.sensor.rawValue) ?? DetectionSensorRole.sensor - } - set { - UserDefaults.standard.set(newValue.rawValue, forKey: "detectionSensorRole") - } - } - static var enableSmartPosition: Bool { - get { - UserDefaults.standard.bool(forKey: "enableSmartPosition") - } - set { - UserDefaults.standard.set(newValue, forKey: "enableSmartPosition") - } - } - static var modemPreset: Int { - get { - UserDefaults.standard.integer(forKey: "modemPreset") - } - set { - UserDefaults.standard.set(newValue, forKey: "modemPreset") - } - } - static var firmwareVersion: String { - get { - UserDefaults.standard.string(forKey: "firmwareVersion") ?? "0.0.0" - } - set { - UserDefaults.standard.set(newValue, forKey: "firmwareVersion") - } - } -} -//import Foundation -// -//@propertyWrapper -//struct UserDefault { -// let key: UserDefaults.Keys -// let defaultValue: T -// -// init(_ key: UserDefaults.Keys, defaultValue: T) { -// self.key = key -// self.defaultValue = defaultValue -// } -// -// var wrappedValue: T { -// get { -// UserDefaults.standard.object(forKey: key.rawValue) as? T ?? defaultValue -// } -// set { -// UserDefaults.standard.set(newValue, forKey: key.rawValue) -// } -// } -//} -// -//extension UserDefaults { -// enum Keys: String, CaseIterable { -// case preferredPeripheralId -// case preferredPeripheralNum -// case provideLocation -// case provideLocationInterval -// case mapLayer -// case meshMapDistance -// case enableMapWaypoints -// case meshMapRecentering -// case meshMapShowNodeHistory -// case meshMapShowRouteLines -// case enableMapConvexHull -// case enableMapRecentering -// case enableMapNodeHistoryPins -// case enableMapRouteLines -// case enableMapTraffic -// case enableMapPointsOfInterest -// case enableOfflineMaps -// case enableOfflineMapsMBTiles -// case mapTileServer -// case enableOverlayServer -// case mapOverlayServer -// case mapTilesAboveLabels -// case mapUseLegacy -// case enableDetectionNotifications -// case detectionSensorRole -// case enableSmartPosition -// case modemPreset -// case firmwareVersion -// } -// -// func reset() { -// Keys.allCases.forEach { removeObject(forKey: $0.rawValue) } -// } -// -// @UserDefault(.preferredPeripheralId, defaultValue: "") -// static var preferredPeripheralId: String -// -// @UserDefault(.preferredPeripheralNum, defaultValue: 0) -// static var preferredPeripheralNum: Int -// -// @UserDefault(.provideLocation, defaultValue: false) -// static var provideLocation: Bool -// -// @UserDefault(.provideLocationInterval, defaultValue: 0) -// static var provideLocationInterval: Int -// -// @UserDefault(.mapLayer, defaultValue: .standard) -// static var mapLayer: MapLayer -// -// @UserDefault(.meshMapDistance, defaultValue: 800000) -// static var meshMapDistance: Double -// -// @UserDefault(.enableMapWaypoints, defaultValue: false) -// static var enableMapWaypoints: Bool -// -// @UserDefault(.enableMapRecentering, defaultValue: false) -// static var enableMapRecentering: Bool -// -// @UserDefault(.enableMapNodeHistoryPins, defaultValue: false) -// static var enableMapNodeHistoryPins: Bool -// -// @UserDefault(.enableMapRouteLines, defaultValue: false) -// static var enableMapRouteLines: Bool -// -// @UserDefault(.enableMapConvexHull, defaultValue: false) -// static var enableMapConvexHull: Bool -// -// @UserDefault(.enableMapTraffic, defaultValue: false) -// static var enableMapTraffic: Bool -// -// @UserDefault(.enableMapPointsOfInterest, defaultValue: false) -// static var enableMapPointsOfInterest: Bool -// -// @UserDefault(.enableOfflineMaps, defaultValue: false) -// static var enableOfflineMaps: Bool -// -// @UserDefault(.enableOfflineMapsMBTiles, defaultValue: false) -// static var enableOfflineMapsMBTiles: Bool -// -// @UserDefault(.mapTileServer, defaultValue: .openStreetMap) -// static var mapTileServer: MapTileServer -// -// @UserDefault(.enableOverlayServer, defaultValue: false) -// static var enableOverlayServer: Bool -// -// @UserDefault(.mapOverlayServer, defaultValue: .baseReReflectivityCurrent) -// static var mapOverlayServer: MapOverlayServer -// -// @UserDefault(.mapTilesAboveLabels, defaultValue: false) -// static var mapTilesAboveLabels: Bool -// -// @UserDefault(.mapUseLegacy, defaultValue: false) -// static var mapUseLegacy: Bool -// -// @UserDefault(.enableDetectionNotifications, defaultValue: false) -// static var enableDetectionNotifications: Bool -// -// @UserDefault(.detectionSensorRole, defaultValue: .sensor) -// static var detectionSensorRole: DetectionSensorRole -// -// @UserDefault(.enableSmartPosition, defaultValue: false) -// static var enableSmartPosition: Bool -// -// @UserDefault(.modemPreset, defaultValue: 0) -// static var modemPreset: Int -// -// @UserDefault(.firmwareVersion, defaultValue: "0.0.0") -// static var firmwareVersion: String -//} + @UserDefault(.preferredPeripheralId, defaultValue: "") + static var preferredPeripheralId: String + + @UserDefault(.preferredPeripheralNum, defaultValue: 0) + static var preferredPeripheralNum: Int + + @UserDefault(.provideLocation, defaultValue: false) + static var provideLocation: Bool + + @UserDefault(.provideLocationInterval, defaultValue: 0) + static var provideLocationInterval: Int + + @UserDefault(.mapLayer, defaultValue: .standard) + static var mapLayer: MapLayer + + @UserDefault(.meshMapDistance, defaultValue: 800000) + static var meshMapDistance: Double + + @UserDefault(.enableMapWaypoints, defaultValue: false) + static var enableMapWaypoints: Bool + + @UserDefault(.enableMapRecentering, defaultValue: false) + static var enableMapRecentering: Bool + + @UserDefault(.enableMapNodeHistoryPins, defaultValue: false) + static var enableMapNodeHistoryPins: Bool + + @UserDefault(.enableMapRouteLines, defaultValue: false) + static var enableMapRouteLines: Bool + + @UserDefault(.enableMapConvexHull, defaultValue: false) + static var enableMapConvexHull: Bool + + @UserDefault(.enableMapTraffic, defaultValue: false) + static var enableMapTraffic: Bool + + @UserDefault(.enableMapPointsOfInterest, defaultValue: false) + static var enableMapPointsOfInterest: Bool + + @UserDefault(.enableOfflineMaps, defaultValue: false) + static var enableOfflineMaps: Bool + + @UserDefault(.enableOfflineMapsMBTiles, defaultValue: false) + static var enableOfflineMapsMBTiles: Bool + + @UserDefault(.mapTileServer, defaultValue: .openStreetMap) + static var mapTileServer: MapTileServer + + @UserDefault(.enableOverlayServer, defaultValue: false) + static var enableOverlayServer: Bool + + @UserDefault(.mapOverlayServer, defaultValue: .baseReReflectivityCurrent) + static var mapOverlayServer: MapOverlayServer + + @UserDefault(.mapTilesAboveLabels, defaultValue: false) + static var mapTilesAboveLabels: Bool + + @UserDefault(.mapUseLegacy, defaultValue: false) + static var mapUseLegacy: Bool + + @UserDefault(.enableDetectionNotifications, defaultValue: false) + static var enableDetectionNotifications: Bool + + @UserDefault(.detectionSensorRole, defaultValue: .sensor) + static var detectionSensorRole: DetectionSensorRole + + @UserDefault(.enableSmartPosition, defaultValue: false) + static var enableSmartPosition: Bool + + @UserDefault(.modemPreset, defaultValue: 0) + static var modemPreset: Int + + @UserDefault(.firmwareVersion, defaultValue: "0.0.0") + static var firmwareVersion: String +} diff --git a/Meshtastic/Views/Settings/Config/Module/DetectionSensorConfig.swift b/Meshtastic/Views/Settings/Config/Module/DetectionSensorConfig.swift index 54570be2..bd00c76e 100644 --- a/Meshtastic/Views/Settings/Config/Module/DetectionSensorConfig.swift +++ b/Meshtastic/Views/Settings/Config/Module/DetectionSensorConfig.swift @@ -6,7 +6,7 @@ // import SwiftUI -enum DetectionSensorRole: String, CaseIterable, Equatable { +enum DetectionSensorRole: String, CaseIterable, Equatable, Decodable { case sensor case client var description: String { From d9ab828d4a9eb56cb0932f7ee4ce6304428ff160 Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Tue, 26 Mar 2024 19:17:13 -0700 Subject: [PATCH 27/34] Fix icon for isonline icon --- Meshtastic/Views/Nodes/Helpers/NodeListFilter.swift | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Meshtastic/Views/Nodes/Helpers/NodeListFilter.swift b/Meshtastic/Views/Nodes/Helpers/NodeListFilter.swift index a49fa9f2..79574927 100644 --- a/Meshtastic/Views/Nodes/Helpers/NodeListFilter.swift +++ b/Meshtastic/Views/Nodes/Helpers/NodeListFilter.swift @@ -52,6 +52,7 @@ struct NodeListFilter: View { } icon: { Image(systemName: "checkmark.circle.fill") .foregroundColor(.green) + .symbolRenderingMode(.hierarchical) } } .toggleStyle(SwitchToggleStyle(tint: .accentColor)) @@ -124,7 +125,7 @@ struct NodeListFilter: View { .padding(.bottom) #endif } - .presentationDetents([.fraction(0.35), .fraction(0.45)]) + .presentationDetents([.fraction(0.40), .fraction(0.50)]) .presentationDragIndicator(.visible) } } From 8149863621e7302b11876190525f839add91c407 Mon Sep 17 00:00:00 2001 From: ChDel Date: Tue, 26 Mar 2024 19:40:48 -0700 Subject: [PATCH 28/34] Allow for numerical enum to decode --- Meshtastic/Extensions/UserDefaults.swift | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/Meshtastic/Extensions/UserDefaults.swift b/Meshtastic/Extensions/UserDefaults.swift index c714efbf..a93a4114 100644 --- a/Meshtastic/Extensions/UserDefaults.swift +++ b/Meshtastic/Extensions/UserDefaults.swift @@ -29,7 +29,8 @@ struct UserDefault { let storedValue = UserDefaults.standard.object(forKey: key.rawValue) guard let storedValue, - let data = ("\"\(storedValue)\"".data(using: .utf8)), + let jsonString = (storedValue as? String != nil) ? "\"\(storedValue)\"" : "\(storedValue)", + let data = jsonString.data(using: .utf8), let value = (try? JSONDecoder().decode(T.self, from: data)) else { return defaultValue } return value @@ -73,6 +74,7 @@ extension UserDefaults { case enableSmartPosition case modemPreset case firmwareVersion + case testIntEnum } func reset() { @@ -153,4 +155,13 @@ extension UserDefaults { @UserDefault(.firmwareVersion, defaultValue: "0.0.0") static var firmwareVersion: String + + @UserDefault(.testIntEnum, defaultValue: .one) + static var testIntEnum: TestIntEnum +} + +enum TestIntEnum: Int, Decodable { + case one = 1 + case two + case three } From 78095caf08cfe185524ba7b1c2f5656e600c7f0b Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Tue, 26 Mar 2024 19:49:30 -0700 Subject: [PATCH 29/34] Fix firmware version comparison, filter the node list when it appears --- Meshtastic/Helpers/Mqtt/MqttClientProxyManager.swift | 7 ++++--- Meshtastic/Views/Nodes/NodeList.swift | 1 + 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/Meshtastic/Helpers/Mqtt/MqttClientProxyManager.swift b/Meshtastic/Helpers/Mqtt/MqttClientProxyManager.swift index 5a1a33d4..80555aea 100644 --- a/Meshtastic/Helpers/Mqtt/MqttClientProxyManager.swift +++ b/Meshtastic/Helpers/Mqtt/MqttClientProxyManager.swift @@ -36,8 +36,9 @@ class MqttClientProxyManager { defaultServerPort = Int(fullHost.components(separatedBy: ":")[1]) ?? (useSsl ? 8883 : 1883) } } - let minimumVersion = "2.3.0" - let latestVersion = minimumVersion.compare(UserDefaults.firmwareVersion, options: .numeric) == .orderedSame + let minimumVersion = "2.3.3" + let currentVersion = UserDefaults.firmwareVersion + let supportedVersion = minimumVersion.compare(currentVersion, options: .numeric) == .orderedAscending || minimumVersion.compare(currentVersion, options: .numeric) == .orderedSame if let host = host { let port = defaultServerPort @@ -45,7 +46,7 @@ class MqttClientProxyManager { let password = node.mqttConfig?.password let root = node.mqttConfig?.root?.count ?? 0 > 0 ? node.mqttConfig?.root : "msh" let prefix = root! - topic = prefix + (latestVersion ? "/2/e" : "/2/c") + "/#" + topic = prefix + (supportedVersion ? "/2/e" : "/2/c") + "/#" let qos = CocoaMQTTQoS(rawValue: UInt8(1))! connect(host: host, port: port, useSsl: useSsl, username: username, password: password, topic: topic, qos: qos, cleanSession: true) } diff --git a/Meshtastic/Views/Nodes/NodeList.swift b/Meshtastic/Views/Nodes/NodeList.swift index a750658b..8a63d2a9 100644 --- a/Meshtastic/Views/Nodes/NodeList.swift +++ b/Meshtastic/Views/Nodes/NodeList.swift @@ -267,6 +267,7 @@ struct NodeList: View { if self.bleManager.context == nil { self.bleManager.context = context } + searchNodeList() } } From 1c9d9b65b59fa1a94e7ef3ef42cb4ae2f38499af Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Tue, 26 Mar 2024 23:36:11 -0700 Subject: [PATCH 30/34] Hook up ui for new fixed position set and remove admin messages. --- Meshtastic.xcodeproj/project.pbxproj | 2 + Meshtastic/Helpers/BLEManager.swift | 65 +++++++++++++++++-- .../Settings/Config/PositionConfig.swift | 42 ++++++++++-- 3 files changed, 96 insertions(+), 13 deletions(-) diff --git a/Meshtastic.xcodeproj/project.pbxproj b/Meshtastic.xcodeproj/project.pbxproj index 5cdd8853..9339e55d 100644 --- a/Meshtastic.xcodeproj/project.pbxproj +++ b/Meshtastic.xcodeproj/project.pbxproj @@ -187,6 +187,7 @@ DDDB445429F8AD1600EE2349 /* Data.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDDB445329F8AD1600EE2349 /* Data.swift */; }; DDDC22382BA92344002C44F1 /* MeshMapContent.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDDC22372BA92344002C44F1 /* MeshMapContent.swift */; }; DDDCD5702BB26F5C00BE6B60 /* NodeListFilter.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDDCD56F2BB26F5C00BE6B60 /* NodeListFilter.swift */; }; + DDDCD5722BB3E46400BE6B60 /* BLEManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDAF8C5226EB1DF10058C060 /* BLEManager.swift */; }; DDDE59F529AF163D00490C6C /* WidgetKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = DD41A61C29AE7E8E003C5A37 /* WidgetKit.framework */; }; DDDE59F629AF163D00490C6C /* SwiftUI.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = DD41A61E29AE7E8F003C5A37 /* SwiftUI.framework */; }; DDDE59F929AF163D00490C6C /* WidgetsBundle.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDDE59F829AF163D00490C6C /* WidgetsBundle.swift */; }; @@ -1404,6 +1405,7 @@ buildActionMask = 2147483647; files = ( DDD3BBD5292D763200D609B3 /* MeshtasticTests.swift in Sources */, + DDDCD5722BB3E46400BE6B60 /* BLEManager.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; diff --git a/Meshtastic/Helpers/BLEManager.swift b/Meshtastic/Helpers/BLEManager.swift index 980e2870..9327ebc8 100644 --- a/Meshtastic/Helpers/BLEManager.swift +++ b/Meshtastic/Helpers/BLEManager.swift @@ -983,9 +983,7 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate return success } - public func sendPosition(channel: Int32, destNum: Int64, wantResponse: Bool) -> Bool { - var success = false - let fromNodeNum = connectedPeripheral.num + public func getPositionFromPhoneGPS(channel: Int32, destNum: Int64) -> Position? { var positionPacket = Position() let fetchChannelRequest: NSFetchRequest = NSFetchRequest.init(entityName: "ChannelEntity") @@ -993,7 +991,7 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate do { guard let fetchedChannel = try context!.fetch(fetchChannelRequest) as? [ChannelEntity] else { - return false + return nil } if #available(iOS 17.0, macOS 14.0, *) { @@ -1019,8 +1017,8 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate } } else { - if fromNodeNum <= 0 || LocationHelper.currentLocation.distance(from: LocationHelper.DefaultLocation) == 0.0 { - return false + if destNum <= 0 || LocationHelper.currentLocation.distance(from: LocationHelper.DefaultLocation) == 0.0 { + return nil } positionPacket.latitudeI = Int32(LocationHelper.currentLocation.latitude * 1e7) positionPacket.longitudeI = Int32(LocationHelper.currentLocation.longitude * 1e7) @@ -1041,6 +1039,61 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate } } catch { + return nil + } + + return positionPacket + } + + public func setFixedPosition(fromUser: UserEntity, channel: Int32) -> Bool { + var adminPacket = AdminMessage() + guard let positionPacket = getPositionFromPhoneGPS(channel: channel, destNum: fromUser.num) else { + return false + } + adminPacket.setFixedPosition = positionPacket + var meshPacket: MeshPacket = MeshPacket() + meshPacket.to = UInt32(fromUser.num) + meshPacket.from = UInt32(fromUser.num) + meshPacket.id = UInt32.random(in: UInt32(UInt8.max).. Bool { + var adminPacket = AdminMessage() + adminPacket.removeFixedPosition = true + var meshPacket: MeshPacket = MeshPacket() + meshPacket.to = UInt32(fromUser.num) + meshPacket.from = UInt32(fromUser.num) + meshPacket.id = UInt32.random(in: UInt32(UInt8.max).. Bool { + var success = false + let fromNodeNum = connectedPeripheral.num + guard let positionPacket = getPositionFromPhoneGPS(channel: channel, destNum: destNum) else { return false } diff --git a/Meshtastic/Views/Settings/Config/PositionConfig.swift b/Meshtastic/Views/Settings/Config/PositionConfig.swift index b18ff5e6..37bb12e7 100644 --- a/Meshtastic/Views/Settings/Config/PositionConfig.swift +++ b/Meshtastic/Views/Settings/Config/PositionConfig.swift @@ -73,8 +73,10 @@ struct PositionConfig: View { @State var includeHeading = false /// Minimum Version for fixed postion admin messages - @State var minimumVersion = "2.3.3" + @State var minimumVersion = "2.3.2" @State private var supportedVersion = true + @State private var showingSetFixedAlert = false + @State private var showingRemoveFixedAlert = false var body: some View { VStack { @@ -160,19 +162,45 @@ struct PositionConfig: View { VStack(alignment: .leading) { Toggle(isOn: $fixedPosition) { Label("Fixed Position", systemImage: "location.square.fill") - Text("If enabled your current phone location will be sent to the device and will broadcast over the mesh on the position interval. Fixed position will always use the most recent position the device has.") + Text("If enabled your current phone location will be sent to the device and will broadcast over the mesh on the position interval.") } .toggleStyle(SwitchToggleStyle(tint: .accentColor)) - } .onChange(of: fixedPosition) { newFixed in if node != nil && node!.positionConfig != nil { - if newFixed != node!.positionConfig!.fixedPosition { hasChanges = true } - } - if supportedVersion && hasChanges && !newFixed { - // Send Admin message to remove the fixed position + /// Fixed Position is off to start + if !node!.positionConfig!.fixedPosition && newFixed && supportedVersion { + showingSetFixedAlert = true + } else if node!.positionConfig!.fixedPosition && newFixed && supportedVersion { + /// Fixed Position is on to start + showingRemoveFixedAlert = true + } } } + .alert(isPresented: $showingSetFixedAlert) { + Alert( + title: Text("Set Fixed Position"), + message: Text("This will send a current position from your phone and enable fixed position."), + primaryButton: .default(Text("Set")) { + print("Set a fixed position here") + }, + secondaryButton: .cancel(Text("Cancel")) { + fixedPosition = false + } + ) + } + .alert(isPresented: $showingRemoveFixedAlert) { + Alert( + title: Text("Remove Fixed Position"), + message: Text("This will disable fixed position and remove the currently set position."), + primaryButton: .destructive(Text("Remove")) { + print("Remove a fixed position here") + }, + secondaryButton: .cancel(Text("Cancel")) { + fixedPosition = true + } + ) + } } } Section(header: Text("Position Flags")) { From 7c022813a24f0597feef2706bb489fde9d17cb14 Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Wed, 27 Mar 2024 16:06:24 -0700 Subject: [PATCH 31/34] Fixed position updates --- Meshtastic/Enums/PositionConfigEnums.swift | 3 + .../CoreData/PositionEntityExtension.swift | 4 +- .../Helpers/Mqtt/MqttClientProxyManager.swift | 2 +- Meshtastic/Views/Nodes/MeshMap.swift | 4 +- .../Settings/Config/PositionConfig.swift | 113 ++++++++++-------- 5 files changed, 73 insertions(+), 53 deletions(-) diff --git a/Meshtastic/Enums/PositionConfigEnums.swift b/Meshtastic/Enums/PositionConfigEnums.swift index e9d4fec2..0773112c 100644 --- a/Meshtastic/Enums/PositionConfigEnums.swift +++ b/Meshtastic/Enums/PositionConfigEnums.swift @@ -57,6 +57,7 @@ enum GpsUpdateIntervals: Int, CaseIterable, Identifiable { case thirtySeconds = 30 case oneMinute = 60 + case twoMinutes = 120 case fiveMinutes = 300 case tenMinutes = 600 case fifteenMinutes = 900 @@ -74,6 +75,8 @@ enum GpsUpdateIntervals: Int, CaseIterable, Identifiable { return "interval.thirty.seconds".localized case .oneMinute: return "interval.one.minute".localized + case .twoMinutes: + return "interval.two.minutes".localized case .fiveMinutes: return "interval.five.minutes".localized case .tenMinutes: diff --git a/Meshtastic/Extensions/CoreData/PositionEntityExtension.swift b/Meshtastic/Extensions/CoreData/PositionEntityExtension.swift index b24ec821..f1f280e5 100644 --- a/Meshtastic/Extensions/CoreData/PositionEntityExtension.swift +++ b/Meshtastic/Extensions/CoreData/PositionEntityExtension.swift @@ -20,8 +20,8 @@ extension PositionEntity { request.includesSubentities = true request.returnsDistinctResults = true request.sortDescriptors = [NSSortDescriptor(key: "time", ascending: false)] - - let positionPredicate = NSPredicate(format: "nodePosition != nil && (nodePosition.user.shortName != nil || nodePosition.user.shortName != '') && latest == true && time >= %@", Calendar.current.date(byAdding: .day, value: -2, to: Date())! as NSDate) + /// && time >= %@ + let positionPredicate = NSPredicate(format: "nodePosition != nil && (nodePosition.user.shortName != nil || nodePosition.user.shortName != '') && latest == true", Calendar.current.date(byAdding: .day, value: -2, to: Date())! as NSDate) let pointOfInterest = LocationHelper.currentLocation diff --git a/Meshtastic/Helpers/Mqtt/MqttClientProxyManager.swift b/Meshtastic/Helpers/Mqtt/MqttClientProxyManager.swift index 80555aea..4de5f67e 100644 --- a/Meshtastic/Helpers/Mqtt/MqttClientProxyManager.swift +++ b/Meshtastic/Helpers/Mqtt/MqttClientProxyManager.swift @@ -36,7 +36,7 @@ class MqttClientProxyManager { defaultServerPort = Int(fullHost.components(separatedBy: ":")[1]) ?? (useSsl ? 8883 : 1883) } } - let minimumVersion = "2.3.3" + let minimumVersion = "2.3.2" let currentVersion = UserDefaults.firmwareVersion let supportedVersion = minimumVersion.compare(currentVersion, options: .numeric) == .orderedAscending || minimumVersion.compare(currentVersion, options: .numeric) == .orderedSame diff --git a/Meshtastic/Views/Nodes/MeshMap.swift b/Meshtastic/Views/Nodes/MeshMap.swift index 2b17ab92..7f1e803e 100644 --- a/Meshtastic/Views/Nodes/MeshMap.swift +++ b/Meshtastic/Views/Nodes/MeshMap.swift @@ -58,10 +58,8 @@ struct MeshMap: View { .mapControlVisibility(.automatic) } .controlSize(.regular) - .onTapGesture(count: 1, perform: { position in - print(position) - newWaypointCoord = reader.convert(position, from: .local) ?? CLLocationCoordinate2D.init() + newWaypointCoord = reader.convert(position, from: .local) ?? CLLocationCoordinate2D.init() }) .gesture( LongPressGesture(minimumDuration: 0.5) diff --git a/Meshtastic/Views/Settings/Config/PositionConfig.swift b/Meshtastic/Views/Settings/Config/PositionConfig.swift index 37bb12e7..38139d4c 100644 --- a/Meshtastic/Views/Settings/Config/PositionConfig.swift +++ b/Meshtastic/Views/Settings/Config/PositionConfig.swift @@ -73,10 +73,10 @@ struct PositionConfig: View { @State var includeHeading = false /// Minimum Version for fixed postion admin messages - @State var minimumVersion = "2.3.2" + @State var minimumVersion = "2.3.3" @State private var supportedVersion = true @State private var showingSetFixedAlert = false - @State private var showingRemoveFixedAlert = false + //@State private var showingRemoveFixedAlert = false var body: some View { VStack { @@ -158,49 +158,13 @@ struct PositionConfig: View { .foregroundColor(.gray) .font(.callout) } - } else { - VStack(alignment: .leading) { - Toggle(isOn: $fixedPosition) { - Label("Fixed Position", systemImage: "location.square.fill") - Text("If enabled your current phone location will be sent to the device and will broadcast over the mesh on the position interval.") - } - .toggleStyle(SwitchToggleStyle(tint: .accentColor)) - } - .onChange(of: fixedPosition) { newFixed in - if node != nil && node!.positionConfig != nil { - /// Fixed Position is off to start - if !node!.positionConfig!.fixedPosition && newFixed && supportedVersion { - showingSetFixedAlert = true - } else if node!.positionConfig!.fixedPosition && newFixed && supportedVersion { - /// Fixed Position is on to start - showingRemoveFixedAlert = true - } - } - } - .alert(isPresented: $showingSetFixedAlert) { - Alert( - title: Text("Set Fixed Position"), - message: Text("This will send a current position from your phone and enable fixed position."), - primaryButton: .default(Text("Set")) { - print("Set a fixed position here") - }, - secondaryButton: .cancel(Text("Cancel")) { - fixedPosition = false - } - ) - } - .alert(isPresented: $showingRemoveFixedAlert) { - Alert( - title: Text("Remove Fixed Position"), - message: Text("This will disable fixed position and remove the currently set position."), - primaryButton: .destructive(Text("Remove")) { - print("Remove a fixed position here") - }, - secondaryButton: .cancel(Text("Cancel")) { - fixedPosition = true - } - ) + } + VStack(alignment: .leading) { + Toggle(isOn: $fixedPosition) { + Label("Fixed Position", systemImage: "location.square.fill") + Text("If enabled your current phone location will be sent to the device and will broadcast over the mesh on the position interval.") } + .toggleStyle(SwitchToggleStyle(tint: .accentColor)) } } Section(header: Text("Position Flags")) { @@ -304,9 +268,49 @@ struct PositionConfig: View { } } .disabled(self.bleManager.connectedPeripheral == nil || node?.positionConfig == nil) + .alert(node?.positionConfig?.fixedPosition ?? false ? "Remove Fixed Position" : "Set Fixed Position", isPresented: $showingSetFixedAlert) { + Button("Cancel", role: .cancel) { + fixedPosition = !fixedPosition + } + if node?.positionConfig?.fixedPosition ?? false { + Button("Remove", role: .destructive) { + if !bleManager.removeFixedPosition(fromUser: node!.user!, channel: 0) { + print("Set Position Failed") + } + print("Remove a fixed position here") + node?.positionConfig?.fixedPosition = false + do { + try context.save() + print("💾 Updated Position Config with Fixed Position = false") + } catch { + context.rollback() + let nsError = error as NSError + print("💥 Error Saving Position Config Entity \(nsError)") + } + } + } else { + Button("Set") { + if !bleManager.setFixedPosition(fromUser: node!.user!, channel: 0) { + print("Set Position Failed") + } + print("Set a fixed position") + node?.positionConfig?.fixedPosition = true + do { + try context.save() + print("💾 Updated Position Config with Fixed Position = true") + } catch { + context.rollback() + let nsError = error as NSError + print("💥 Error Saving Position Config Entity \(nsError)") + } + } + } + } message: { + Text(node?.positionConfig?.fixedPosition ?? false ? "This will disable fixed position and remove the currently set position." : "This will send a current position from your phone and enable fixed position.") + } SaveConfigButton(node: node, hasChanges: $hasChanges) { - if fixedPosition { + if fixedPosition && !supportedVersion { _ = bleManager.sendPosition(channel: 0, destNum: node?.num ?? 0, wantResponse: true) } let connectedNode = getNodeInfo(id: bleManager.connectedPeripheral.num, context: context) @@ -358,8 +362,6 @@ struct PositionConfig: View { } setPositionValues() supportedVersion = bleManager.connectedVersion == "0.0.0" || self.minimumVersion.compare(bleManager.connectedVersion, options: .numeric) == .orderedAscending || minimumVersion.compare(bleManager.connectedVersion, options: .numeric) == .orderedSame - - // Need to request a PositionConfig from the remote node before allowing changes if bleManager.connectedPeripheral != nil && node?.positionConfig == nil { print("empty position config") @@ -369,6 +371,23 @@ struct PositionConfig: View { } } } + .onChange(of: fixedPosition) { newFixed in + print("Changing Fixed Position Value") + if supportedVersion { + if node != nil && node!.positionConfig != nil { + print("We have a node and position config") + print("We have turned on fixed position \(!node!.positionConfig!.fixedPosition && newFixed)") + /// Fixed Position is off to start + if !node!.positionConfig!.fixedPosition && newFixed { + print("fire alert") + showingSetFixedAlert = true + } else if node!.positionConfig!.fixedPosition && !newFixed { + /// Fixed Position is on to start + showingSetFixedAlert = true + } + } + } + } .onChange(of: deviceGpsEnabled) { newDeviceGps in if node != nil && node!.positionConfig != nil { if newDeviceGps != node!.positionConfig!.deviceGpsEnabled { hasChanges = true } From 4d584f64c9b40b06e01a31eafc19635c1e11bef9 Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Wed, 27 Mar 2024 16:30:58 -0700 Subject: [PATCH 32/34] Flat the map by default so precision circles are prettier --- Meshtastic/Views/Nodes/Helpers/Map/NodeMapSwiftUI.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Meshtastic/Views/Nodes/Helpers/Map/NodeMapSwiftUI.swift b/Meshtastic/Views/Nodes/Helpers/Map/NodeMapSwiftUI.swift index 6bb5c18c..3f316cf5 100644 --- a/Meshtastic/Views/Nodes/Helpers/Map/NodeMapSwiftUI.swift +++ b/Meshtastic/Views/Nodes/Helpers/Map/NodeMapSwiftUI.swift @@ -25,7 +25,7 @@ struct NodeMapSwiftUI: View { @AppStorage("mapLayer") private var selectedMapLayer: MapLayer = .hybrid // Map Configuration @Namespace var mapScope - @State var mapStyle: MapStyle = MapStyle.hybrid(elevation: .realistic, pointsOfInterest: .all, showsTraffic: true) + @State var mapStyle: MapStyle = MapStyle.hybrid(elevation: .flat, pointsOfInterest: .all, showsTraffic: true) @State var position = MapCameraPosition.automatic @State var scene: MKLookAroundScene? @State var isLookingAround = false From 6a7baf1ebe1de6ecf147628c368acc125f846ebf Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Thu, 28 Mar 2024 08:39:34 -0700 Subject: [PATCH 33/34] Add back 48 hour filter to the mesh map, add admin channel tip --- .../CoreData/PositionEntityExtension.swift | 5 ++--- Meshtastic/Tips/ChannelTips.swift | 17 +++++++++++++++++ .../Nodes/Helpers/Map/NodeMapSwiftUI.swift | 2 -- Meshtastic/Views/Settings/Settings.swift | 6 ++++++ de.lproj/Localizable.strings | 2 ++ en.lproj/Localizable.strings | 2 ++ fr.lproj/Localizable.strings | 2 ++ he.lproj/Localizable.strings | 2 ++ pl.lproj/Localizable.strings | 2 ++ zh-Hans.lproj/Localizable.strings | 2 ++ zh-Hant-TW.lproj/Localizable.strings | 2 ++ 11 files changed, 39 insertions(+), 5 deletions(-) diff --git a/Meshtastic/Extensions/CoreData/PositionEntityExtension.swift b/Meshtastic/Extensions/CoreData/PositionEntityExtension.swift index f1f280e5..0221a1da 100644 --- a/Meshtastic/Extensions/CoreData/PositionEntityExtension.swift +++ b/Meshtastic/Extensions/CoreData/PositionEntityExtension.swift @@ -14,14 +14,13 @@ extension PositionEntity { static func allPositionsFetchRequest() -> NSFetchRequest { let request: NSFetchRequest = PositionEntity.fetchRequest() - request.fetchLimit = 300 + request.fetchLimit = 200 //request.fetchBatchSize = 1 request.returnsObjectsAsFaults = false request.includesSubentities = true request.returnsDistinctResults = true request.sortDescriptors = [NSSortDescriptor(key: "time", ascending: false)] - /// && time >= %@ - let positionPredicate = NSPredicate(format: "nodePosition != nil && (nodePosition.user.shortName != nil || nodePosition.user.shortName != '') && latest == true", Calendar.current.date(byAdding: .day, value: -2, to: Date())! as NSDate) + let positionPredicate = NSPredicate(format: "nodePosition != nil && (nodePosition.user.shortName != nil || nodePosition.user.shortName != '') && latest == true && time >= %@", Calendar.current.date(byAdding: .day, value: -2, to: Date())! as NSDate) let pointOfInterest = LocationHelper.currentLocation diff --git a/Meshtastic/Tips/ChannelTips.swift b/Meshtastic/Tips/ChannelTips.swift index 871af684..be241951 100644 --- a/Meshtastic/Tips/ChannelTips.swift +++ b/Meshtastic/Tips/ChannelTips.swift @@ -42,3 +42,20 @@ struct CreateChannelsTip: Tip { Image(systemName: "fibrechannel") } } + +@available(iOS 17.0, macOS 14.0, *) +struct AdminChannelTip: Tip { + + var id: String { + return "tip.channel.admin" + } + var title: Text { + Text("tip.channel.admin.title") + } + var message: Text? { + Text("tip.channel.admin.message") + } + var image: Image? { + Image(systemName: "fibrechannel") + } +} diff --git a/Meshtastic/Views/Nodes/Helpers/Map/NodeMapSwiftUI.swift b/Meshtastic/Views/Nodes/Helpers/Map/NodeMapSwiftUI.swift index 3f316cf5..9cbb352b 100644 --- a/Meshtastic/Views/Nodes/Helpers/Map/NodeMapSwiftUI.swift +++ b/Meshtastic/Views/Nodes/Helpers/Map/NodeMapSwiftUI.swift @@ -41,8 +41,6 @@ struct NodeMapSwiftUI: View { ), animation: .none) private var waypoints: FetchedResults - - var body: some View { var mostRecent = node.positions?.lastObject as? PositionEntity diff --git a/Meshtastic/Views/Settings/Settings.swift b/Meshtastic/Views/Settings/Settings.swift index f0b221aa..b990b0f4 100644 --- a/Meshtastic/Views/Settings/Settings.swift +++ b/Meshtastic/Views/Settings/Settings.swift @@ -6,6 +6,9 @@ // import SwiftUI +#if canImport(TipKit) +import TipKit +#endif struct Settings: View { @Environment(\.managedObjectContext) var context @@ -131,6 +134,9 @@ struct Settings: View { } } } + if #available(iOS 17.0, macOS 14.0, *) { + TipView(AdminChannelTip(), arrowEdge: .top) + } } else { if bleManager.connectedPeripheral != nil { Text("Connected Node \(node?.user?.longName ?? "unknown".localized)") diff --git a/de.lproj/Localizable.strings b/de.lproj/Localizable.strings index c3d3d0fc..bf9e8d39 100644 --- a/de.lproj/Localizable.strings +++ b/de.lproj/Localizable.strings @@ -320,6 +320,8 @@ "timestamp"="Timestamp"; "tip.bluetooth.connect.title"="Connected LoRa Radio"; "tip.bluetooth.connect.message"="Shows information for the Lora radio currently connected via bluetooth. You can swipe left to disconnect the radio and long press to view stats or start the live activity."; +"tip.channel.admin.title"="Admin Channel"; +"tip.channel.admin.message"="Admin channel detected: Select a node from the drop down to manage connected or remote devices."; "tip.channels.create.title"="Manage Channels"; "tip.channels.create.message"="Most data on your mesh is sent over the primary channel. You can set up secondary channels to create additional messaging groups secured by their own key. [Channel config tips](https://meshtastic.org/docs/configuration/radio/channels/)"; "tip.channels.share.title"="Sharing Meshtastic Channels"; diff --git a/en.lproj/Localizable.strings b/en.lproj/Localizable.strings index e0d113b8..e031fd63 100644 --- a/en.lproj/Localizable.strings +++ b/en.lproj/Localizable.strings @@ -334,6 +334,8 @@ "timestamp"="Timestamp"; "tip.bluetooth.connect.title"="Connected Radio"; "tip.bluetooth.connect.message"="Shows information for the Lora radio connected via bluetooth. You can swipe left to disconnect the radio and long press to view stats or start the live activity."; +"tip.channel.admin.title"="Admin Channel"; +"tip.channel.admin.message"="Admin channel detected: Select a node from the drop down to manage connected or remote devices."; "tip.channels.create.title"="Manage Channels"; "tip.channels.create.message"="Most data on your mesh is sent over the primary channel. You can set up secondary channels to create additional messaging groups secured by their own key. [Channel config tips](https://meshtastic.org/docs/configuration/tips/)"; "tip.channels.share.title"="Sharing Meshtastic Channels"; diff --git a/fr.lproj/Localizable.strings b/fr.lproj/Localizable.strings index b41a35a4..34a507da 100644 --- a/fr.lproj/Localizable.strings +++ b/fr.lproj/Localizable.strings @@ -302,6 +302,8 @@ "tip.bluetooth.connect.message"="Affiche les informations de la radio Lora connectée via le bluetooth. Vous pouvez faire un glissé vers la gauche pour déconnecter la radio et un appui long pour voir les statistiques ou démarrer l'activité en direct."; "tip.channels.create.title"="Gérer les canaux"; "tip.channels.create.message"="La pluspart des données de votre maillage sont envoyées sur le canal principal. Vous pouvez définir des canaux secondaires pour créer des groupes de messagerie additionnelle sécurisés avec leur propre clé. [Conseils de configuration du canal](https://meshtastic.org/docs/configuration/tips/)"; +"tip.channel.admin.title"="Admin Channel"; +"tip.channel.admin.message"="Admin channel detected: Select a node from the drop down to manage connected or remote devices."; "tip.channels.share.title"="Partage des canaux Meshtastic"; "tip.channels.share.message"="Un code QR Meshtastic contient la configuration LoRa et les valeurs de canal nécessaires pour communiquer. La plupart des activités du maillage ont lieu sur le canal principal requis. Si vous ne partagez pas votre canal principal, votre premier canal partagé devient le canal principal de l’autre réseau. Les autres canaux sont pour les groupes privés, chacun avec sa propre clé."; "tip.messages.title"="Messages"; diff --git a/he.lproj/Localizable.strings b/he.lproj/Localizable.strings index e9dc8ce1..d7e0a05d 100644 --- a/he.lproj/Localizable.strings +++ b/he.lproj/Localizable.strings @@ -324,6 +324,8 @@ "timestamp"="שעה/תאריך"; "tip.bluetooth.connect.title"="מכשיר מחובר"; "tip.bluetooth.connect.message"="מראה מידע אודות מכשיר המשטסטיק המחובר כעת לבלוטוס. ניתן לגרור שמאלה להתנתקות או לחיצה ארוכה לראות סטטיסטיקה או להתחיל פעילות."; +"tip.channel.admin.title"="Admin Channel"; +"tip.channel.admin.message"="Admin channel detected: Select a node from the drop down to manage connected or remote devices."; "tip.channels.create.title"="Manage Channels"; "tip.channels.create.message"="Most data on your mesh is sent over the primary channel. You can set up secondary channels to create additional messaging groups secured by their own key. [Channel config tips](https://meshtastic.org/docs/configuration/radio/channels/)"; "tip.channels.share.title"="משתף ערוצי משטסטיק"; diff --git a/pl.lproj/Localizable.strings b/pl.lproj/Localizable.strings index 5e194834..6a3b18b8 100644 --- a/pl.lproj/Localizable.strings +++ b/pl.lproj/Localizable.strings @@ -321,6 +321,8 @@ "timestamp"="Znacznik czasu"; "tip.bluetooth.connect.title"="Connected LoRa Radio"; "tip.bluetooth.connect.message"="Shows information for the Lora radio currently connected via bluetooth. You can swipe left to disconnect the radio and long press to view stats or start the live activity."; +"tip.channel.admin.title"="Admin Channel"; +"tip.channel.admin.message"="Admin channel detected: Select a node from the drop down to manage connected or remote devices."; "tip.channels.create.title"="Manage Channels"; "tip.channels.create.message"="Most data on your mesh is sent over the primary channel. You can set up secondary channels to create additional messaging groups secured by their own key. [Channel config tips](https://meshtastic.org/docs/configuration/radio/channels/)"; "tip.channels.share.title"="Sharing Meshtastic Channels"; diff --git a/zh-Hans.lproj/Localizable.strings b/zh-Hans.lproj/Localizable.strings index d4521ef2..886cab0a 100644 --- a/zh-Hans.lproj/Localizable.strings +++ b/zh-Hans.lproj/Localizable.strings @@ -320,6 +320,8 @@ "timestamp"="时间戳"; "tip.bluetooth.connect.title"="连接到 LoRa 电台"; "tip.bluetooth.connect.message"="显示当前通过蓝牙连接的 Lora 电台的信息。您可以向左滑动断开电台,长按查看统计信息或开始实时活动。"; +"tip.channel.admin.title"="Admin Channel"; +"tip.channel.admin.message"="Admin channel detected: Select a node from the drop down to manage connected or remote devices."; "tip.channels.create.title"="Manage Channels"; "tip.channels.create.message"="Most data on your mesh is sent over the primary channel. You can set up secondary channels to create additional messaging groups secured by their own key. [Channel config tips](https://meshtastic.org/docs/configuration/radio/channels/)"; "tip.channels.share.title"="共享 Meshtastic 频道"; diff --git a/zh-Hant-TW.lproj/Localizable.strings b/zh-Hant-TW.lproj/Localizable.strings index 8bef85fd..68562e56 100644 --- a/zh-Hant-TW.lproj/Localizable.strings +++ b/zh-Hant-TW.lproj/Localizable.strings @@ -319,6 +319,8 @@ "timestamp"="時間戳記"; "tip.bluetooth.connect.title"="連接到 LoRa 電台"; "tip.bluetooth.connect.message"="顯示目前通過藍芽連接的 Lora 電台的信息。您可以向左滑動斷開電台,長按查看統計訊息或開始即時活動。"; +"tip.channel.admin.title"="Admin Channel"; +"tip.channel.admin.message"="Admin channel detected: Select a node from the drop down to manage connected or remote devices."; "tip.channels.create.title"="管理頻道"; "tip.channels.create.message"="現在 Mesh 上的資料會通過主通道發送。您可以設定輔助通道來建立由自己的金鑰保護的其他訊息組 [頻道設定提示](https://meshtastic.org/docs/configuration/radio/channels/)"; "tip.channels.share.title"="共享 Meshtastic 頻道"; From 9c0430f956ae6af70517912cd8aed11fedd142bd Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Thu, 28 Mar 2024 08:44:10 -0700 Subject: [PATCH 34/34] Remove extra comment --- Meshtastic/Extensions/UserDefaults.swift | 6 ------ 1 file changed, 6 deletions(-) diff --git a/Meshtastic/Extensions/UserDefaults.swift b/Meshtastic/Extensions/UserDefaults.swift index a93a4114..ab5f2ba9 100644 --- a/Meshtastic/Extensions/UserDefaults.swift +++ b/Meshtastic/Extensions/UserDefaults.swift @@ -4,12 +4,6 @@ // // Copyright(c) Garth Vander Houwen 4/24/23. // -// -// UserDefaults.swift -// Meshtastic -// -// Copyright(c) Garth Vander Houwen 4/24/23. -// import Foundation