From 05397db3a4a4339fb626a19c7e2ae95dc6316100 Mon Sep 17 00:00:00 2001 From: Ben Meadors Date: Tue, 13 May 2025 19:43:43 -0500 Subject: [PATCH 01/16] Add shared contact functionality (WIP) Feel free to hijack and make it not terrible --- Meshtastic.xcodeproj/project.pbxproj | 8 ++ .../CoreData/NodeInfoEntityToNodeInfo.swift | 26 ++++++ Meshtastic/Helpers/BLEManager.swift | 49 ++++++++++ Meshtastic/Meshtastic.entitlements | 1 + Meshtastic/MeshtasticApp.swift | 61 ++++++++++++- .../Nodes/Helpers/ShareContactQRDialog.swift | 91 +++++++++++++++++++ Meshtastic/Views/Nodes/NodeList.swift | 13 +++ 7 files changed, 247 insertions(+), 2 deletions(-) create mode 100644 Meshtastic/Extensions/CoreData/NodeInfoEntityToNodeInfo.swift create mode 100644 Meshtastic/Views/Nodes/Helpers/ShareContactQRDialog.swift diff --git a/Meshtastic.xcodeproj/project.pbxproj b/Meshtastic.xcodeproj/project.pbxproj index 66649df5..7f4c9e0d 100644 --- a/Meshtastic.xcodeproj/project.pbxproj +++ b/Meshtastic.xcodeproj/project.pbxproj @@ -7,6 +7,8 @@ objects = { /* Begin PBXBuildFile section */ + 108FFECB2DD3F43C00BFAA81 /* ShareContactQRDialog.swift in Sources */ = {isa = PBXBuildFile; fileRef = 108FFECA2DD3F43C00BFAA81 /* ShareContactQRDialog.swift */; }; + 108FFECD2DD4005600BFAA81 /* NodeInfoEntityToNodeInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 108FFECC2DD4005600BFAA81 /* NodeInfoEntityToNodeInfo.swift */; }; 231B3F212D087A4C0069A07D /* MetricTableColumn.swift in Sources */ = {isa = PBXBuildFile; fileRef = 231B3F202D087A4C0069A07D /* MetricTableColumn.swift */; }; 231B3F222D087A4C0069A07D /* MetricsColumnList.swift in Sources */ = {isa = PBXBuildFile; fileRef = 231B3F1F2D087A4C0069A07D /* MetricsColumnList.swift */; }; 231B3F252D087C3C0069A07D /* EnvironmentDefaultColumns.swift in Sources */ = {isa = PBXBuildFile; fileRef = 231B3F242D087C3C0069A07D /* EnvironmentDefaultColumns.swift */; }; @@ -274,6 +276,8 @@ /* End PBXCopyFilesBuildPhase section */ /* Begin PBXFileReference section */ + 108FFECA2DD3F43C00BFAA81 /* ShareContactQRDialog.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShareContactQRDialog.swift; sourceTree = ""; }; + 108FFECC2DD4005600BFAA81 /* NodeInfoEntityToNodeInfo.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NodeInfoEntityToNodeInfo.swift; sourceTree = ""; }; 231B3F1F2D087A4C0069A07D /* MetricsColumnList.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MetricsColumnList.swift; sourceTree = ""; }; 231B3F202D087A4C0069A07D /* MetricTableColumn.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MetricTableColumn.swift; sourceTree = ""; }; 231B3F242D087C3C0069A07D /* EnvironmentDefaultColumns.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EnvironmentDefaultColumns.swift; sourceTree = ""; }; @@ -701,6 +705,7 @@ DD007BB12AA59B9A00F5FA12 /* CoreData */ = { isa = PBXGroup; children = ( + 108FFECC2DD4005600BFAA81 /* NodeInfoEntityToNodeInfo.swift */, 2344A2AA2D66973D00170A77 /* ManagedAttributePropertyWrapper.swift */, DD58C5F12919AD3C00D5BEFB /* ChannelEntityExtension.swift */, 6DEDA55B2A9592F900321D2E /* MessageEntityExtension.swift */, @@ -1101,6 +1106,7 @@ DDDB26402AABEF7B003AFCB7 /* Helpers */ = { isa = PBXGroup; children = ( + 108FFECA2DD3F43C00BFAA81 /* ShareContactQRDialog.swift */, 231B3F232D087C020069A07D /* Metrics Columns */, DDAD49EB2AFAE82500B4425D /* Map */, DDDB26432AAC0206003AFCB7 /* NodeDetail.swift */, @@ -1396,6 +1402,7 @@ DD4640202AFF10F4002A5ECB /* WaypointForm.swift in Sources */, 233E99C12D849D6000CC3A77 /* DistanceCompactWidget.swift in Sources */, DD769E0328D18BF1001A3F05 /* DeviceMetricsLog.swift in Sources */, + 108FFECD2DD4005600BFAA81 /* NodeInfoEntityToNodeInfo.swift in Sources */, DDAF8C5326EB1DF10058C060 /* BLEManager.swift in Sources */, DD15E4F32B8BA56E00654F61 /* PaxCounterConfig.swift in Sources */, DDDB445229F8ACF900EE2349 /* Date.swift in Sources */, @@ -1486,6 +1493,7 @@ DDA1C48E28DB49D3009933EC /* ChannelRoles.swift in Sources */, DDD5BB092C285DDC007E03CA /* AppLog.swift in Sources */, DD8ED9C8289CE4B900B3B0AB /* RoutingError.swift in Sources */, + 108FFECB2DD3F43C00BFAA81 /* ShareContactQRDialog.swift in Sources */, 233E99C52D84A0B600CC3A77 /* CompactWidget.swift in Sources */, DDC1B81A2AB5377B00C71E39 /* MessagesTips.swift in Sources */, DD964FC62975DBFD007C176F /* QueryCoreData.swift in Sources */, diff --git a/Meshtastic/Extensions/CoreData/NodeInfoEntityToNodeInfo.swift b/Meshtastic/Extensions/CoreData/NodeInfoEntityToNodeInfo.swift new file mode 100644 index 00000000..c34f62a0 --- /dev/null +++ b/Meshtastic/Extensions/CoreData/NodeInfoEntityToNodeInfo.swift @@ -0,0 +1,26 @@ +// NodeInfoEntityToNodeInfo.swift +// Meshtastic +// +// Utility to convert NodeInfoEntity (Core Data) to NodeInfo (protobuf) + +import Foundation +import MeshtasticProtobufs + +extension NodeInfoEntity { + func toProto() -> NodeInfo { + var userProto = User() + if let user = self.user { + userProto.id = user.userId ?? "" + userProto.longName = user.longName ?? "" + userProto.shortName = user.shortName ?? "" + userProto.hwModel = HardwareModel(rawValue: Int(user.hwModelId))!; userProto.isLicensed = user.isLicensed + userProto.role = Config.DeviceConfig.Role(rawValue: Int(user.role))! + userProto.publicKey = user.publicKey?.subdata(in: 0.. Bool { + if isConnected { + + let decodedString = base64UrlString.base64urlToBase64() + if let decodedData = Data(base64Encoded: decodedString) { + do { + let contact: SharedContact = try SharedContact(serializedBytes: decodedData) + var adminPacket = AdminMessage() + adminPacket.addContact = contact + var meshPacket: MeshPacket = MeshPacket() + meshPacket.to = UInt32(connectedPeripheral.num) + meshPacket.from = UInt32(connectedPeripheral.num) + meshPacket.id = UInt32.random(in: UInt32(UInt8.max).. Int64 { var adminPacket = AdminMessage() adminPacket.setOwner = config diff --git a/Meshtastic/Meshtastic.entitlements b/Meshtastic/Meshtastic.entitlements index abcef61d..0d2247ee 100644 --- a/Meshtastic/Meshtastic.entitlements +++ b/Meshtastic/Meshtastic.entitlements @@ -7,6 +7,7 @@ com.apple.developer.associated-domains applinks:meshtastic.org/e/* + applinks:meshtastic.org/v/* com.apple.developer.weatherkit diff --git a/Meshtastic/MeshtasticApp.swift b/Meshtastic/MeshtasticApp.swift index 5c82c257..7ba8b701 100644 --- a/Meshtastic/MeshtasticApp.swift +++ b/Meshtastic/MeshtasticApp.swift @@ -4,6 +4,7 @@ import SwiftUI import CoreData import OSLog import TipKit +import MeshtasticProtobufs @main struct MeshtasticAppleApp: App { @@ -87,7 +88,63 @@ struct MeshtasticAppleApp: App { Logger.mesh.debug("Some sort of URL was received \(url, privacy: .public)") self.incomingUrl = url - if url.absoluteString.lowercased().contains("meshtastic.org/e/#") { + if url.absoluteString.lowercased().contains("meshtastic.org/v/#") { + if let components = self.incomingUrl?.absoluteString.components(separatedBy: "#") { + // Extract contact information from the URL + if let contactData = components.last { + + let decodedString = contactData.base64urlToBase64() + if let decodedData = Data(base64Encoded: decodedString) { + do { + let contact = try MeshtasticProtobufs.SharedContact(serializedBytes: decodedData) + + // Show an alert to confirm adding the contact + let alertController = UIAlertController( + title: "Add Contact", + message: "Would you like to add \(contact.user.longName) as a contact?", + preferredStyle: .alert + ) + + alertController.addAction(UIAlertAction( + title: "Yes", + style: .default, + handler: { _ in + let success = BLEManager.shared.addContactFromURL(base64UrlString: contactData) + Logger.services.debug("Contact added from URL: \(success ? "success" : "failed")") + } + )) + + alertController.addAction(UIAlertAction( + title: "No", + style: .cancel, + handler: nil + )) + + // Present the alert + if let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene, + let rootViewController = windowScene.windows.first?.rootViewController { + rootViewController.present(alertController, animated: true) + } + Logger.services.debug("Contact data extracted from URL: \(contactData, privacy: .public)") + } catch { + Logger.services.error("Failed to parse contact data: \(error.localizedDescription, privacy: .public)") + + // Show error alert to user + if let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene, + let rootViewController = windowScene.windows.first?.rootViewController { + let errorAlert = UIAlertController( + title: "Error", + message: "Could not process contact information. Invalid format.", + preferredStyle: .alert + ) + errorAlert.addAction(UIAlertAction(title: "OK", style: .default)) + rootViewController.present(errorAlert, animated: true) + } + } + } + } + } + } else if url.absoluteString.lowercased().contains("meshtastic.org/e/#") { if let components = self.incomingUrl?.absoluteString.components(separatedBy: "#") { self.addChannels = Bool(self.incomingUrl?["add"] ?? "false") ?? false if self.incomingUrl?.absoluteString.lowercased().contains("?") != nil { @@ -119,7 +176,7 @@ struct MeshtasticAppleApp: App { .displayFrequency(.immediate) ] ) - } + } } .onChange(of: scenePhase) { (_, newScenePhase) in switch newScenePhase { diff --git a/Meshtastic/Views/Nodes/Helpers/ShareContactQRDialog.swift b/Meshtastic/Views/Nodes/Helpers/ShareContactQRDialog.swift new file mode 100644 index 00000000..23b65ce6 --- /dev/null +++ b/Meshtastic/Views/Nodes/Helpers/ShareContactQRDialog.swift @@ -0,0 +1,91 @@ +// ShareContactQRDialog.swift +// Meshtastic +// +// Created by GitHub Copilot on 5/13/25. + +import SwiftUI +import CoreImage.CIFilterBuiltins +#if canImport(UIKit) +import UIKit +#endif +import CoreData +import MeshtasticProtobufs +import OSLog + +struct ShareContactQRDialog: View { + let node: NodeInfo + @Environment(\.dismiss) private var dismiss + + var qrString: String { + var contact = SharedContact() + contact.nodeNum = node.num + contact.user = node.user + + do { + let contactString = try contact.serializedData().base64EncodedString() + return ("https://meshtastic.org/v/#" + contactString.base64ToBase64url()) + } catch { + Logger.services.error("Error serializing contact: \(error)") + return "Error generating QR code" + } + + } + + var qrImage: UIImage { + let context = CIContext() + let filter = CIFilter.qrCodeGenerator() + filter.setValue(Data(qrString.utf8), forKey: "inputMessage") + let transform = CGAffineTransform(scaleX: 10, y: 10) + if let outputImage = filter.outputImage?.transformed(by: transform), + let cgimg = context.createCGImage(outputImage, from: outputImage.extent) { + return UIImage(cgImage: cgimg) + } + return UIImage(systemName: "xmark.circle") ?? UIImage() + } + + var body: some View { + VStack(spacing: 20) { + Text("Share Contact QR") + .font(.title2) + .padding(.top) + Text(node.user.longName) + .font(.headline) + Image(uiImage: qrImage) + .interpolation(.none) + .resizable() + .scaledToFit() + .frame(width: 220, height: 220) + .background(Color(.systemBackground)) + .cornerRadius(16) + .shadow(radius: 4) + Text("Scan this QR code to add \(node.user.longName) to another device.") + .font(.subheadline) + .multilineTextAlignment(.center) + .foregroundColor(.secondary) + Button("Done") { dismiss() } + .buttonStyle(.borderedProminent) + .padding(.bottom) + } + .padding() + .frame(maxWidth: 350) + } +} + +#if DEBUG +struct ShareContactQRDialog_Previews: PreviewProvider { + static var previews: some View { + var node = NodeInfo() + node.num = 123456 + var userProto = User() + userProto.id = "!1234" + userProto.longName = "Bud" + userProto.shortName = "Bud" + userProto.hwModel = HardwareModel(rawValue:1)!; + userProto.role = Config.DeviceConfig.Role(rawValue: 1)! + userProto.publicKey = Data() + node.user = userProto + + return ShareContactQRDialog(node: node) + } +} +#endif diff --git a/Meshtastic/Views/Nodes/NodeList.swift b/Meshtastic/Views/Nodes/NodeList.swift index a17a19d0..168aa018 100644 --- a/Meshtastic/Views/Nodes/NodeList.swift +++ b/Meshtastic/Views/Nodes/NodeList.swift @@ -40,6 +40,8 @@ struct NodeList: View { @State private var isPresentingPositionFailedAlert = false @State private var isPresentingDeleteNodeAlert = false @State private var deleteNodeId: Int64 = 0 + @State private var isPresentingShareContactQR = false + @State private var shareContactNode: NodeInfoEntity? var boolFilters: [Bool] {[ isFavorite, @@ -78,6 +80,12 @@ struct NodeList: View { /// Allow users to mute notifications for a node even if they are not connected if let user = node.user { NodeAlertsButton(context: context, node: node, user: user) + Button(action: { + shareContactNode = node + isPresentingShareContactQR = true + }) { + Label("Share Contact QR", systemImage: "qrcode") + } } if let connectedNode { /// Favoriting a node requires being connected @@ -223,6 +231,11 @@ struct NodeList: View { } } } + } + .sheet(isPresented: $isPresentingShareContactQR) { + if let node = shareContactNode { + ShareContactQRDialog(node: node.toProto()) + } } .navigationSplitViewColumnWidth(min: 100, ideal: 250, max: 500) .navigationBarItems( From 6fc6a8fcfa78f7b1a529d076fe464e5787d640f3 Mon Sep 17 00:00:00 2001 From: Ben Meadors Date: Tue, 13 May 2025 19:59:12 -0500 Subject: [PATCH 02/16] Fix QR code generation being slow as ball and log error --- Meshtastic/Helpers/BLEManager.swift | 1 + Meshtastic/Views/Nodes/Helpers/ShareContactQRDialog.swift | 1 - 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/Meshtastic/Helpers/BLEManager.swift b/Meshtastic/Helpers/BLEManager.swift index 5da739ae..cb759ffb 100644 --- a/Meshtastic/Helpers/BLEManager.swift +++ b/Meshtastic/Helpers/BLEManager.swift @@ -1811,6 +1811,7 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate } } catch { + Logger.data.error("Failed to decode contact data: \(error.localizedDescription, privacy: .public)") return false } } diff --git a/Meshtastic/Views/Nodes/Helpers/ShareContactQRDialog.swift b/Meshtastic/Views/Nodes/Helpers/ShareContactQRDialog.swift index 23b65ce6..30f63747 100644 --- a/Meshtastic/Views/Nodes/Helpers/ShareContactQRDialog.swift +++ b/Meshtastic/Views/Nodes/Helpers/ShareContactQRDialog.swift @@ -54,7 +54,6 @@ struct ShareContactQRDialog: View { .interpolation(.none) .resizable() .scaledToFit() - .frame(width: 220, height: 220) .background(Color(.systemBackground)) .cornerRadius(16) .shadow(radius: 4) From 9a5b3d7c65ea271edb77c5e88f003c1701cd6c28 Mon Sep 17 00:00:00 2001 From: Ben Meadors Date: Tue, 13 May 2025 20:02:40 -0500 Subject: [PATCH 03/16] Update Meshtastic/Views/Nodes/NodeList.swift Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- Meshtastic/Views/Nodes/NodeList.swift | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Meshtastic/Views/Nodes/NodeList.swift b/Meshtastic/Views/Nodes/NodeList.swift index 168aa018..370f03eb 100644 --- a/Meshtastic/Views/Nodes/NodeList.swift +++ b/Meshtastic/Views/Nodes/NodeList.swift @@ -235,6 +235,8 @@ struct NodeList: View { .sheet(isPresented: $isPresentingShareContactQR) { if let node = shareContactNode { ShareContactQRDialog(node: node.toProto()) + } else { + EmptyView() } } .navigationSplitViewColumnWidth(min: 100, ideal: 250, max: 500) From bb43a1ec9da7895cd61dfcc291d7f02c248db01c Mon Sep 17 00:00:00 2001 From: Ben Meadors Date: Tue, 13 May 2025 20:03:33 -0500 Subject: [PATCH 04/16] Update Meshtastic/Views/Nodes/Helpers/ShareContactQRDialog.swift Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- Meshtastic/Views/Nodes/Helpers/ShareContactQRDialog.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Meshtastic/Views/Nodes/Helpers/ShareContactQRDialog.swift b/Meshtastic/Views/Nodes/Helpers/ShareContactQRDialog.swift index 30f63747..76901ff6 100644 --- a/Meshtastic/Views/Nodes/Helpers/ShareContactQRDialog.swift +++ b/Meshtastic/Views/Nodes/Helpers/ShareContactQRDialog.swift @@ -26,7 +26,7 @@ struct ShareContactQRDialog: View { return ("https://meshtastic.org/v/#" + contactString.base64ToBase64url()) } catch { Logger.services.error("Error serializing contact: \(error)") - return "Error generating QR code" + return "" } } From 1b00ea786069aa6b47dc4559111d82a1fa82850b Mon Sep 17 00:00:00 2001 From: Ben Meadors Date: Tue, 13 May 2025 20:03:44 -0500 Subject: [PATCH 05/16] Update Meshtastic/MeshtasticApp.swift Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- Meshtastic/MeshtasticApp.swift | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Meshtastic/MeshtasticApp.swift b/Meshtastic/MeshtasticApp.swift index 7ba8b701..5d8df782 100644 --- a/Meshtastic/MeshtasticApp.swift +++ b/Meshtastic/MeshtasticApp.swift @@ -89,9 +89,9 @@ struct MeshtasticAppleApp: App { Logger.mesh.debug("Some sort of URL was received \(url, privacy: .public)") self.incomingUrl = url if url.absoluteString.lowercased().contains("meshtastic.org/v/#") { - if let components = self.incomingUrl?.absoluteString.components(separatedBy: "#") { - // Extract contact information from the URL - if let contactData = components.last { + let components = self.incomingUrl?.absoluteString.components(separatedBy: "#") ?? [] + // Extract contact information from the URL + if let contactData = components.last { let decodedString = contactData.base64urlToBase64() if let decodedData = Data(base64Encoded: decodedString) { From 2857ed3dc9f74c81df21143382096cb3e14af190 Mon Sep 17 00:00:00 2001 From: Ben Meadors Date: Tue, 13 May 2025 20:03:58 -0500 Subject: [PATCH 06/16] Update Meshtastic/Extensions/CoreData/NodeInfoEntityToNodeInfo.swift Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- Meshtastic/Extensions/CoreData/NodeInfoEntityToNodeInfo.swift | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Meshtastic/Extensions/CoreData/NodeInfoEntityToNodeInfo.swift b/Meshtastic/Extensions/CoreData/NodeInfoEntityToNodeInfo.swift index c34f62a0..935ad956 100644 --- a/Meshtastic/Extensions/CoreData/NodeInfoEntityToNodeInfo.swift +++ b/Meshtastic/Extensions/CoreData/NodeInfoEntityToNodeInfo.swift @@ -13,7 +13,8 @@ extension NodeInfoEntity { userProto.id = user.userId ?? "" userProto.longName = user.longName ?? "" userProto.shortName = user.shortName ?? "" - userProto.hwModel = HardwareModel(rawValue: Int(user.hwModelId))!; userProto.isLicensed = user.isLicensed + userProto.hwModel = HardwareModel(rawValue: Int(user.hwModelId))! + userProto.isLicensed = user.isLicensed userProto.role = Config.DeviceConfig.Role(rawValue: Int(user.role))! userProto.publicKey = user.publicKey?.subdata(in: 0.. Date: Tue, 13 May 2025 20:06:07 -0500 Subject: [PATCH 07/16] Copilot jacked up my braces --- Meshtastic/MeshtasticApp.swift | 1 - 1 file changed, 1 deletion(-) diff --git a/Meshtastic/MeshtasticApp.swift b/Meshtastic/MeshtasticApp.swift index 5d8df782..1a4ba85b 100644 --- a/Meshtastic/MeshtasticApp.swift +++ b/Meshtastic/MeshtasticApp.swift @@ -143,7 +143,6 @@ struct MeshtasticAppleApp: App { } } } - } } else if url.absoluteString.lowercased().contains("meshtastic.org/e/#") { if let components = self.incomingUrl?.absoluteString.components(separatedBy: "#") { self.addChannels = Bool(self.incomingUrl?["add"] ?? "false") ?? false From bf55f3526de146e2bb1ad3eafecc8f515208835a Mon Sep 17 00:00:00 2001 From: Ben Meadors Date: Tue, 13 May 2025 20:09:30 -0500 Subject: [PATCH 08/16] Update Meshtastic/Extensions/CoreData/NodeInfoEntityToNodeInfo.swift Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- Meshtastic/Extensions/CoreData/NodeInfoEntityToNodeInfo.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Meshtastic/Extensions/CoreData/NodeInfoEntityToNodeInfo.swift b/Meshtastic/Extensions/CoreData/NodeInfoEntityToNodeInfo.swift index 935ad956..a1a0a03c 100644 --- a/Meshtastic/Extensions/CoreData/NodeInfoEntityToNodeInfo.swift +++ b/Meshtastic/Extensions/CoreData/NodeInfoEntityToNodeInfo.swift @@ -15,7 +15,7 @@ extension NodeInfoEntity { userProto.shortName = user.shortName ?? "" userProto.hwModel = HardwareModel(rawValue: Int(user.hwModelId))! userProto.isLicensed = user.isLicensed - userProto.role = Config.DeviceConfig.Role(rawValue: Int(user.role))! + userProto.role = Config.DeviceConfig.Role(rawValue: Int(user.role)) ?? .unknown userProto.publicKey = user.publicKey?.subdata(in: 0.. Date: Tue, 13 May 2025 20:09:35 -0500 Subject: [PATCH 09/16] Update Meshtastic/Extensions/CoreData/NodeInfoEntityToNodeInfo.swift Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- Meshtastic/Extensions/CoreData/NodeInfoEntityToNodeInfo.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Meshtastic/Extensions/CoreData/NodeInfoEntityToNodeInfo.swift b/Meshtastic/Extensions/CoreData/NodeInfoEntityToNodeInfo.swift index a1a0a03c..05f3ec3b 100644 --- a/Meshtastic/Extensions/CoreData/NodeInfoEntityToNodeInfo.swift +++ b/Meshtastic/Extensions/CoreData/NodeInfoEntityToNodeInfo.swift @@ -13,7 +13,7 @@ extension NodeInfoEntity { userProto.id = user.userId ?? "" userProto.longName = user.longName ?? "" userProto.shortName = user.shortName ?? "" - userProto.hwModel = HardwareModel(rawValue: Int(user.hwModelId))! + userProto.hwModel = HardwareModel(rawValue: Int(user.hwModelId)) ?? .unknown userProto.isLicensed = user.isLicensed userProto.role = Config.DeviceConfig.Role(rawValue: Int(user.role)) ?? .unknown userProto.publicKey = user.publicKey?.subdata(in: 0.. Date: Tue, 13 May 2025 20:11:27 -0500 Subject: [PATCH 10/16] Defaults --- Meshtastic/Extensions/CoreData/NodeInfoEntityToNodeInfo.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Meshtastic/Extensions/CoreData/NodeInfoEntityToNodeInfo.swift b/Meshtastic/Extensions/CoreData/NodeInfoEntityToNodeInfo.swift index 05f3ec3b..73706d2e 100644 --- a/Meshtastic/Extensions/CoreData/NodeInfoEntityToNodeInfo.swift +++ b/Meshtastic/Extensions/CoreData/NodeInfoEntityToNodeInfo.swift @@ -13,9 +13,9 @@ extension NodeInfoEntity { userProto.id = user.userId ?? "" userProto.longName = user.longName ?? "" userProto.shortName = user.shortName ?? "" - userProto.hwModel = HardwareModel(rawValue: Int(user.hwModelId)) ?? .unknown + userProto.hwModel = HardwareModel(rawValue: Int(user.hwModelId)) ?? .unset userProto.isLicensed = user.isLicensed - userProto.role = Config.DeviceConfig.Role(rawValue: Int(user.role)) ?? .unknown + userProto.role = Config.DeviceConfig.Role(rawValue: Int(user.role)) ?? .client userProto.publicKey = user.publicKey?.subdata(in: 0.. Date: Tue, 13 May 2025 19:56:06 -0700 Subject: [PATCH 11/16] Import Contact App Shortcut and add a share url button to the page --- Localizable.xcstrings | 116 +++++++++++------- Meshtastic.xcodeproj/project.pbxproj | 4 + Meshtastic/AppIntents/AddContactIntent.swift | 51 ++++++++ .../Nodes/Helpers/ShareContactQRDialog.swift | 7 ++ 4 files changed, 132 insertions(+), 46 deletions(-) create mode 100644 Meshtastic/AppIntents/AddContactIntent.swift diff --git a/Localizable.xcstrings b/Localizable.xcstrings index b97eb676..23cb27cf 100644 --- a/Localizable.xcstrings +++ b/Localizable.xcstrings @@ -7201,6 +7201,9 @@ } } } + }, + "Contact URL" : { + }, "Contacts (%@)" : { "localizations" : { @@ -9994,6 +9997,9 @@ } } } + }, + "Done" : { + }, "Double Tap as Button" : { "localizations" : { @@ -15082,6 +15088,12 @@ } } } + }, + "Import Contact" : { + + }, + "Import Meshtastic Node %@ as a contact" : { + }, "Import Route" : { "localizations" : { @@ -22193,6 +22205,52 @@ } } }, + "Position config received: %@" : { + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Positionskonfiguration empfangen: %@" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Configuration de la position reçue : %@" + } + }, + "he" : { + "stringUnit" : { + "state" : "translated", + "value" : "הגדרות מיקום התקבלו: %@" + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Configurazione della posizione ricevuta: %@" + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Odebrano konfigurację pozycji: %@" + } + }, + "se" : { + "stringUnit" : { + "state" : "translated", + "value" : "Positionskonfiguration mottagen: %@" + } + }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Конфигурација позиције примљена: %@" + } + } + } + }, "Position Exchange Failed" : { "localizations" : { "it" : { @@ -22467,52 +22525,6 @@ } } }, - "Position config received: %@" : { - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Positionskonfiguration empfangen: %@" - } - }, - "fr" : { - "stringUnit" : { - "state" : "translated", - "value" : "Configuration de la position reçue : %@" - } - }, - "he" : { - "stringUnit" : { - "state" : "translated", - "value" : "הגדרות מיקום התקבלו: %@" - } - }, - "it" : { - "stringUnit" : { - "state" : "translated", - "value" : "Configurazione della posizione ricevuta: %@" - } - }, - "pl" : { - "stringUnit" : { - "state" : "translated", - "value" : "Odebrano konfigurację pozycji: %@" - } - }, - "se" : { - "stringUnit" : { - "state" : "translated", - "value" : "Positionskonfiguration mottagen: %@" - } - }, - "sr" : { - "stringUnit" : { - "state" : "translated", - "value" : "Конфигурација позиције примљена: %@" - } - } - } - }, "Power" : { "localizations" : { "de" : { @@ -26021,6 +26033,9 @@ } } } + }, + "Scan this QR code to add %@ to another device." : { + }, "Screen on for" : { "localizations" : { @@ -28033,6 +28048,9 @@ } } } + }, + "Share Contact QR" : { + }, "Share QR Code" : { "localizations" : { @@ -29711,6 +29729,9 @@ } } } + }, + "Takes a Meshtastic contact URL and saves it to the nodes database" : { + }, "Tapback" : { "localizations" : { @@ -30891,6 +30912,9 @@ } } } + }, + "The URL for the node to import" : { + }, "There has been no response to a request for device metadata over the admin channel for this node." : { "localizations" : { diff --git a/Meshtastic.xcodeproj/project.pbxproj b/Meshtastic.xcodeproj/project.pbxproj index 7f4c9e0d..fb48dfe1 100644 --- a/Meshtastic.xcodeproj/project.pbxproj +++ b/Meshtastic.xcodeproj/project.pbxproj @@ -58,6 +58,7 @@ 8D3F8A412D44C2A6009EAAA4 /* PowerMetricsLog.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8D3F8A402D44C2A6009EAAA4 /* PowerMetricsLog.swift */; }; B399E8A42B6F486400E4488E /* RetryButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = B399E8A32B6F486400E4488E /* RetryButton.swift */; }; B3E905B12B71F7F300654D07 /* TextMessageField.swift in Sources */ = {isa = PBXBuildFile; fileRef = B3E905B02B71F7F300654D07 /* TextMessageField.swift */; }; + BC10380F2DD4334400B00BFA /* AddContactIntent.swift in Sources */ = {isa = PBXBuildFile; fileRef = BC10380E2DD4333C00B00BFA /* AddContactIntent.swift */; }; BC47C2EF2CE0017D008245CA /* MessageNodeIntent.swift in Sources */ = {isa = PBXBuildFile; fileRef = BC47C2EE2CE0017D008245CA /* MessageNodeIntent.swift */; }; BC6B45FF2CB2F98900723CEB /* SaveChannelSettingsIntent.swift in Sources */ = {isa = PBXBuildFile; fileRef = BC6B45FE2CB2F98900723CEB /* SaveChannelSettingsIntent.swift */; }; BCB613812C67290800485544 /* SendWaypointIntent.swift in Sources */ = {isa = PBXBuildFile; fileRef = BCB613802C67290800485544 /* SendWaypointIntent.swift */; }; @@ -322,6 +323,7 @@ 8D3F8A402D44C2A6009EAAA4 /* PowerMetricsLog.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PowerMetricsLog.swift; sourceTree = ""; }; B399E8A32B6F486400E4488E /* RetryButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RetryButton.swift; sourceTree = ""; }; B3E905B02B71F7F300654D07 /* TextMessageField.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TextMessageField.swift; sourceTree = ""; }; + BC10380E2DD4333C00B00BFA /* AddContactIntent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddContactIntent.swift; sourceTree = ""; }; BC47C2EE2CE0017D008245CA /* MessageNodeIntent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageNodeIntent.swift; sourceTree = ""; }; BC5EBA3B2D002A2000C442FF /* MessageNodeIntent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageNodeIntent.swift; sourceTree = ""; }; BC6B45FE2CB2F98900723CEB /* SaveChannelSettingsIntent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SaveChannelSettingsIntent.swift; sourceTree = ""; }; @@ -676,6 +678,7 @@ BCB6137F2C6728E700485544 /* AppIntents */ = { isa = PBXGroup; children = ( + BC10380E2DD4333C00B00BFA /* AddContactIntent.swift */, BC5EBA3B2D002A2000C442FF /* MessageNodeIntent.swift */, BCB613802C67290800485544 /* SendWaypointIntent.swift */, BCB613822C672A2600485544 /* MessageChannelIntent.swift */, @@ -1551,6 +1554,7 @@ 2344A2AB2D66974300170A77 /* ManagedAttributePropertyWrapper.swift in Sources */, BCB613832C672A2600485544 /* MessageChannelIntent.swift in Sources */, D93068D52B812B700066FBC8 /* MessageDestination.swift in Sources */, + BC10380F2DD4334400B00BFA /* AddContactIntent.swift in Sources */, DDA9515E2BC6F56F00CEA535 /* IndoorAirQuality.swift in Sources */, DDDB444E29F8AB0E00EE2349 /* Int.swift in Sources */, DD3CC6BC28E366DF00FA9159 /* Meshtastic.xcdatamodeld in Sources */, diff --git a/Meshtastic/AppIntents/AddContactIntent.swift b/Meshtastic/AppIntents/AddContactIntent.swift new file mode 100644 index 00000000..ff8ca149 --- /dev/null +++ b/Meshtastic/AppIntents/AddContactIntent.swift @@ -0,0 +1,51 @@ +// +// AddContactIntent.swift +// Meshtastic +// +// Created by Benjamin Faershtein on 5/13/25. +// + +import AppIntents +import MeshtasticProtobufs + +struct AddContactIntent: AppIntent { + static var title: LocalizedStringResource = "Import Contact" + static var description: IntentDescription = "Takes a Meshtastic contact URL and saves it to the nodes database" + + @Parameter(title: "Contact URL", description: "The URL for the node to import") + var contactUrl: URL + + // Define the function that performs the main logic + func perform() async throws -> some IntentResult { + // Ensure the BLE Manager is connected + if !BLEManager.shared.isConnected { + throw AppIntentErrors.AppIntentError.notConnected + } + + if contactUrl.absoluteString.lowercased().contains("meshtastic.org/v/#") { + + let components = self.contactUrl.absoluteString.components(separatedBy: "#") + // Extract contact information from the URL + if let contactData = components.last { + + let decodedString = contactData.base64urlToBase64() + if let decodedData = Data(base64Encoded: decodedString) { + do { + let success = BLEManager.shared.addContactFromURL(base64UrlString: contactData) + if !success { + throw AppIntentErrors.AppIntentError.message("Failed to import contact") + } + + } catch { + throw AppIntentErrors.AppIntentError.message("Failed to parse contact data: \(error.localizedDescription)") + + } + } + } + // Return a success result + return .result() + } else { + throw AppIntentErrors.AppIntentError.message("The URL is not a valid Meshtastic contact link") + } + } +} diff --git a/Meshtastic/Views/Nodes/Helpers/ShareContactQRDialog.swift b/Meshtastic/Views/Nodes/Helpers/ShareContactQRDialog.swift index 76901ff6..e5d679b9 100644 --- a/Meshtastic/Views/Nodes/Helpers/ShareContactQRDialog.swift +++ b/Meshtastic/Views/Nodes/Helpers/ShareContactQRDialog.swift @@ -61,6 +61,13 @@ struct ShareContactQRDialog: View { .font(.subheadline) .multilineTextAlignment(.center) .foregroundColor(.secondary) + ShareLink("Share QR Code & Link", + item: Image(uiImage: qrImage), + subject: Text("Import Meshtastic Node \(node.user.shortName) as a contact"), + message: Text(qrString), + preview: SharePreview("Import Meshtastic Node \(node.user.shortName) as a contact", + image: Image(uiImage: qrImage)) + ) Button("Done") { dismiss() } .buttonStyle(.borderedProminent) .padding(.bottom) From 863f51b697e496a6202a79514ebc2027ac3d0293 Mon Sep 17 00:00:00 2001 From: Ben Meadors Date: Wed, 14 May 2025 15:42:38 -0500 Subject: [PATCH 12/16] Update ShareContactQRDialog.swift --- Meshtastic/Views/Nodes/Helpers/ShareContactQRDialog.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Meshtastic/Views/Nodes/Helpers/ShareContactQRDialog.swift b/Meshtastic/Views/Nodes/Helpers/ShareContactQRDialog.swift index e5d679b9..5d361fbe 100644 --- a/Meshtastic/Views/Nodes/Helpers/ShareContactQRDialog.swift +++ b/Meshtastic/Views/Nodes/Helpers/ShareContactQRDialog.swift @@ -63,9 +63,9 @@ struct ShareContactQRDialog: View { .foregroundColor(.secondary) ShareLink("Share QR Code & Link", item: Image(uiImage: qrImage), - subject: Text("Import Meshtastic Node \(node.user.shortName) as a contact"), + subject: Text("Add Meshtastic Node \(node.user.shortName) as a contact"), message: Text(qrString), - preview: SharePreview("Import Meshtastic Node \(node.user.shortName) as a contact", + preview: SharePreview("Add Meshtastic Node \(node.user.shortName) as a contact", image: Image(uiImage: qrImage)) ) Button("Done") { dismiss() } From de3f834c0999e4e2796f94e30fd9d6bcb3dd6c44 Mon Sep 17 00:00:00 2001 From: Ben Meadors Date: Wed, 14 May 2025 15:48:49 -0500 Subject: [PATCH 13/16] Updated protobufs --- .../Sources/meshtastic/deviceonly.pb.swift | 23 +++++++++++++++++++ .../Sources/meshtastic/mesh.pb.swift | 23 +++++++++++++++++++ .../Sources/meshtastic/module_config.pb.swift | 10 ++++++++ .../Sources/meshtastic/mqtt.pb.swift | 11 +++++++++ .../Sources/meshtastic/telemetry.pb.swift | 8 +++++++ protobufs | 2 +- 6 files changed, 76 insertions(+), 1 deletion(-) diff --git a/MeshtasticProtobufs/Sources/meshtastic/deviceonly.pb.swift b/MeshtasticProtobufs/Sources/meshtastic/deviceonly.pb.swift index 72248719..cbcbda13 100644 --- a/MeshtasticProtobufs/Sources/meshtastic/deviceonly.pb.swift +++ b/MeshtasticProtobufs/Sources/meshtastic/deviceonly.pb.swift @@ -100,9 +100,22 @@ public struct UserLite: @unchecked Sendable { /// This is sent out to other nodes on the mesh to allow them to compute a shared secret key. public var publicKey: Data = Data() + /// + /// Whether or not the node can be messaged + public var isUnmessagable: Bool { + get {return _isUnmessagable ?? false} + set {_isUnmessagable = newValue} + } + /// Returns true if `isUnmessagable` has been explicitly set. + public var hasIsUnmessagable: Bool {return self._isUnmessagable != nil} + /// Clears the value of `isUnmessagable`. Subsequent reads from it will return its default value. + public mutating func clearIsUnmessagable() {self._isUnmessagable = nil} + public var unknownFields = SwiftProtobuf.UnknownStorage() public init() {} + + fileprivate var _isUnmessagable: Bool? = nil } public struct NodeInfoLite: @unchecked Sendable { @@ -512,6 +525,7 @@ extension UserLite: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationB 5: .standard(proto: "is_licensed"), 6: .same(proto: "role"), 7: .standard(proto: "public_key"), + 9: .standard(proto: "is_unmessagable"), ] public mutating func decodeMessage(decoder: inout D) throws { @@ -527,12 +541,17 @@ extension UserLite: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationB case 5: try { try decoder.decodeSingularBoolField(value: &self.isLicensed) }() case 6: try { try decoder.decodeSingularEnumField(value: &self.role) }() case 7: try { try decoder.decodeSingularBytesField(value: &self.publicKey) }() + case 9: try { try decoder.decodeSingularBoolField(value: &self._isUnmessagable) }() default: break } } } public 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.macaddr.isEmpty { try visitor.visitSingularBytesField(value: self.macaddr, fieldNumber: 1) } @@ -554,6 +573,9 @@ extension UserLite: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationB if !self.publicKey.isEmpty { try visitor.visitSingularBytesField(value: self.publicKey, fieldNumber: 7) } + try { if let v = self._isUnmessagable { + try visitor.visitSingularBoolField(value: v, fieldNumber: 9) + } }() try unknownFields.traverse(visitor: &visitor) } @@ -565,6 +587,7 @@ extension UserLite: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationB if lhs.isLicensed != rhs.isLicensed {return false} if lhs.role != rhs.role {return false} if lhs.publicKey != rhs.publicKey {return false} + if lhs._isUnmessagable != rhs._isUnmessagable {return false} if lhs.unknownFields != rhs.unknownFields {return false} return true } diff --git a/MeshtasticProtobufs/Sources/meshtastic/mesh.pb.swift b/MeshtasticProtobufs/Sources/meshtastic/mesh.pb.swift index 14948e13..d59ec2ed 100644 --- a/MeshtasticProtobufs/Sources/meshtastic/mesh.pb.swift +++ b/MeshtasticProtobufs/Sources/meshtastic/mesh.pb.swift @@ -1503,9 +1503,22 @@ public struct User: @unchecked Sendable { /// This is sent out to other nodes on the mesh to allow them to compute a shared secret key. public var publicKey: Data = Data() + /// + /// Whether or not the node can be messaged + public var isUnmessagable: Bool { + get {return _isUnmessagable ?? false} + set {_isUnmessagable = newValue} + } + /// Returns true if `isUnmessagable` has been explicitly set. + public var hasIsUnmessagable: Bool {return self._isUnmessagable != nil} + /// Clears the value of `isUnmessagable`. Subsequent reads from it will return its default value. + public mutating func clearIsUnmessagable() {self._isUnmessagable = nil} + public var unknownFields = SwiftProtobuf.UnknownStorage() public init() {} + + fileprivate var _isUnmessagable: Bool? = nil } /// @@ -3751,6 +3764,7 @@ extension User: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, 6: .standard(proto: "is_licensed"), 7: .same(proto: "role"), 8: .standard(proto: "public_key"), + 9: .standard(proto: "is_unmessagable"), ] public mutating func decodeMessage(decoder: inout D) throws { @@ -3767,12 +3781,17 @@ extension User: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, case 6: try { try decoder.decodeSingularBoolField(value: &self.isLicensed) }() case 7: try { try decoder.decodeSingularEnumField(value: &self.role) }() case 8: try { try decoder.decodeSingularBytesField(value: &self.publicKey) }() + case 9: try { try decoder.decodeSingularBoolField(value: &self._isUnmessagable) }() default: break } } } public 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.id.isEmpty { try visitor.visitSingularStringField(value: self.id, fieldNumber: 1) } @@ -3797,6 +3816,9 @@ extension User: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, if !self.publicKey.isEmpty { try visitor.visitSingularBytesField(value: self.publicKey, fieldNumber: 8) } + try { if let v = self._isUnmessagable { + try visitor.visitSingularBoolField(value: v, fieldNumber: 9) + } }() try unknownFields.traverse(visitor: &visitor) } @@ -3809,6 +3831,7 @@ extension User: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, if lhs.isLicensed != rhs.isLicensed {return false} if lhs.role != rhs.role {return false} if lhs.publicKey != rhs.publicKey {return false} + if lhs._isUnmessagable != rhs._isUnmessagable {return false} if lhs.unknownFields != rhs.unknownFields {return false} return true } diff --git a/MeshtasticProtobufs/Sources/meshtastic/module_config.pb.swift b/MeshtasticProtobufs/Sources/meshtastic/module_config.pb.swift index f717951d..c2e81366 100644 --- a/MeshtasticProtobufs/Sources/meshtastic/module_config.pb.swift +++ b/MeshtasticProtobufs/Sources/meshtastic/module_config.pb.swift @@ -345,6 +345,10 @@ public struct ModuleConfig: Sendable { /// Bits of precision for the location sent (default of 32 is full precision). public var positionPrecision: UInt32 = 0 + /// + /// Whether we have opted-in to report our location to the map + public var shouldReportLocation: Bool = false + public var unknownFields = SwiftProtobuf.UnknownStorage() public init() {} @@ -1647,6 +1651,7 @@ extension ModuleConfig.MapReportSettings: SwiftProtobuf.Message, SwiftProtobuf._ public static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ 1: .standard(proto: "publish_interval_secs"), 2: .standard(proto: "position_precision"), + 3: .standard(proto: "should_report_location"), ] public mutating func decodeMessage(decoder: inout D) throws { @@ -1657,6 +1662,7 @@ extension ModuleConfig.MapReportSettings: SwiftProtobuf.Message, SwiftProtobuf._ switch fieldNumber { case 1: try { try decoder.decodeSingularUInt32Field(value: &self.publishIntervalSecs) }() case 2: try { try decoder.decodeSingularUInt32Field(value: &self.positionPrecision) }() + case 3: try { try decoder.decodeSingularBoolField(value: &self.shouldReportLocation) }() default: break } } @@ -1669,12 +1675,16 @@ extension ModuleConfig.MapReportSettings: SwiftProtobuf.Message, SwiftProtobuf._ if self.positionPrecision != 0 { try visitor.visitSingularUInt32Field(value: self.positionPrecision, fieldNumber: 2) } + if self.shouldReportLocation != false { + try visitor.visitSingularBoolField(value: self.shouldReportLocation, fieldNumber: 3) + } try unknownFields.traverse(visitor: &visitor) } public static func ==(lhs: ModuleConfig.MapReportSettings, rhs: ModuleConfig.MapReportSettings) -> Bool { if lhs.publishIntervalSecs != rhs.publishIntervalSecs {return false} if lhs.positionPrecision != rhs.positionPrecision {return false} + if lhs.shouldReportLocation != rhs.shouldReportLocation {return false} if lhs.unknownFields != rhs.unknownFields {return false} return true } diff --git a/MeshtasticProtobufs/Sources/meshtastic/mqtt.pb.swift b/MeshtasticProtobufs/Sources/meshtastic/mqtt.pb.swift index 006fd9c8..80508b5d 100644 --- a/MeshtasticProtobufs/Sources/meshtastic/mqtt.pb.swift +++ b/MeshtasticProtobufs/Sources/meshtastic/mqtt.pb.swift @@ -116,6 +116,11 @@ public struct MapReport: Sendable { /// Number of online nodes (heard in the last 2 hours) this node has in its list that were received locally (not via MQTT) public var numOnlineLocalNodes: UInt32 = 0 + /// + /// User has opted in to share their location (map report) with the mqtt server + /// Controlled by map_report.should_report_location + public var hasOptedReportLocation_p: Bool = false + public var unknownFields = SwiftProtobuf.UnknownStorage() public init() {} @@ -189,6 +194,7 @@ extension MapReport: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementation 11: .same(proto: "altitude"), 12: .standard(proto: "position_precision"), 13: .standard(proto: "num_online_local_nodes"), + 14: .standard(proto: "has_opted_report_location"), ] public mutating func decodeMessage(decoder: inout D) throws { @@ -210,6 +216,7 @@ extension MapReport: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementation case 11: try { try decoder.decodeSingularInt32Field(value: &self.altitude) }() case 12: try { try decoder.decodeSingularUInt32Field(value: &self.positionPrecision) }() case 13: try { try decoder.decodeSingularUInt32Field(value: &self.numOnlineLocalNodes) }() + case 14: try { try decoder.decodeSingularBoolField(value: &self.hasOptedReportLocation_p) }() default: break } } @@ -255,6 +262,9 @@ extension MapReport: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementation if self.numOnlineLocalNodes != 0 { try visitor.visitSingularUInt32Field(value: self.numOnlineLocalNodes, fieldNumber: 13) } + if self.hasOptedReportLocation_p != false { + try visitor.visitSingularBoolField(value: self.hasOptedReportLocation_p, fieldNumber: 14) + } try unknownFields.traverse(visitor: &visitor) } @@ -272,6 +282,7 @@ extension MapReport: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementation if lhs.altitude != rhs.altitude {return false} if lhs.positionPrecision != rhs.positionPrecision {return false} if lhs.numOnlineLocalNodes != rhs.numOnlineLocalNodes {return false} + if lhs.hasOptedReportLocation_p != rhs.hasOptedReportLocation_p {return false} if lhs.unknownFields != rhs.unknownFields {return false} return true } diff --git a/MeshtasticProtobufs/Sources/meshtastic/telemetry.pb.swift b/MeshtasticProtobufs/Sources/meshtastic/telemetry.pb.swift index 90b56546..ccf4cfb4 100644 --- a/MeshtasticProtobufs/Sources/meshtastic/telemetry.pb.swift +++ b/MeshtasticProtobufs/Sources/meshtastic/telemetry.pb.swift @@ -176,6 +176,10 @@ public enum TelemetrySensorType: SwiftProtobuf.Enum, Swift.CaseIterable { /// /// RAKWireless RAK12035 Soil Moisture Sensor Module case rak12035 // = 37 + + /// + /// MAX17261 lipo battery gauge + case max17261 // = 38 case UNRECOGNIZED(Int) public init() { @@ -222,6 +226,7 @@ public enum TelemetrySensorType: SwiftProtobuf.Enum, Swift.CaseIterable { case 35: self = .dfrobotRain case 36: self = .dps310 case 37: self = .rak12035 + case 38: self = .max17261 default: self = .UNRECOGNIZED(rawValue) } } @@ -266,6 +271,7 @@ public enum TelemetrySensorType: SwiftProtobuf.Enum, Swift.CaseIterable { case .dfrobotRain: return 35 case .dps310: return 36 case .rak12035: return 37 + case .max17261: return 38 case .UNRECOGNIZED(let i): return i } } @@ -310,6 +316,7 @@ public enum TelemetrySensorType: SwiftProtobuf.Enum, Swift.CaseIterable { .dfrobotRain, .dps310, .rak12035, + .max17261, ] } @@ -1170,6 +1177,7 @@ extension TelemetrySensorType: SwiftProtobuf._ProtoNameProviding { 35: .same(proto: "DFROBOT_RAIN"), 36: .same(proto: "DPS310"), 37: .same(proto: "RAK12035"), + 38: .same(proto: "MAX17261"), ] } diff --git a/protobufs b/protobufs index 816595c8..47ec99aa 160000 --- a/protobufs +++ b/protobufs @@ -1 +1 @@ -Subproject commit 816595c8bbdfc3b4388e11348ccd043294d58705 +Subproject commit 47ec99aa4c4a2e3fff71fd5170663f0848deb021 From bd70589fe786ad624a9408f0de2685171226a33b Mon Sep 17 00:00:00 2001 From: Ben Meadors Date: Wed, 14 May 2025 16:40:02 -0500 Subject: [PATCH 14/16] Missed a couple --- Localizable.xcstrings | 4 ++-- Meshtastic/AppIntents/AddContactIntent.swift | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/Localizable.xcstrings b/Localizable.xcstrings index 23cb27cf..bff5fa52 100644 --- a/Localizable.xcstrings +++ b/Localizable.xcstrings @@ -15089,10 +15089,10 @@ } } }, - "Import Contact" : { + "Add Contact" : { }, - "Import Meshtastic Node %@ as a contact" : { + "Add Meshtastic Node %@ as a contact" : { }, "Import Route" : { diff --git a/Meshtastic/AppIntents/AddContactIntent.swift b/Meshtastic/AppIntents/AddContactIntent.swift index ff8ca149..17c6b960 100644 --- a/Meshtastic/AppIntents/AddContactIntent.swift +++ b/Meshtastic/AppIntents/AddContactIntent.swift @@ -9,10 +9,10 @@ import AppIntents import MeshtasticProtobufs struct AddContactIntent: AppIntent { - static var title: LocalizedStringResource = "Import Contact" + static var title: LocalizedStringResource = "Add Contact" static var description: IntentDescription = "Takes a Meshtastic contact URL and saves it to the nodes database" - @Parameter(title: "Contact URL", description: "The URL for the node to import") + @Parameter(title: "Contact URL", description: "The URL for the node to add") var contactUrl: URL // Define the function that performs the main logic @@ -33,7 +33,7 @@ struct AddContactIntent: AppIntent { do { let success = BLEManager.shared.addContactFromURL(base64UrlString: contactData) if !success { - throw AppIntentErrors.AppIntentError.message("Failed to import contact") + throw AppIntentErrors.AppIntentError.message("Failed to add contact") } } catch { From 7196596cae1fa46871e4d81e689d15cebc05a5ec Mon Sep 17 00:00:00 2001 From: Ben Meadors Date: Wed, 14 May 2025 16:59:57 -0500 Subject: [PATCH 15/16] Update Localizable.xcstrings Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- Localizable.xcstrings | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/Localizable.xcstrings b/Localizable.xcstrings index bff5fa52..0742d933 100644 --- a/Localizable.xcstrings +++ b/Localizable.xcstrings @@ -7203,7 +7203,14 @@ } }, "Contact URL" : { - + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Contact URL" + } + } + } }, "Contacts (%@)" : { "localizations" : { From 3d04610690ee229fa5240a6d1a496dd4db1abdaf Mon Sep 17 00:00:00 2001 From: Ben Meadors Date: Wed, 14 May 2025 19:35:29 -0500 Subject: [PATCH 16/16] Plumb proto --- Meshtastic/Extensions/CoreData/NodeInfoEntityToNodeInfo.swift | 1 + 1 file changed, 1 insertion(+) diff --git a/Meshtastic/Extensions/CoreData/NodeInfoEntityToNodeInfo.swift b/Meshtastic/Extensions/CoreData/NodeInfoEntityToNodeInfo.swift index 73706d2e..0c487346 100644 --- a/Meshtastic/Extensions/CoreData/NodeInfoEntityToNodeInfo.swift +++ b/Meshtastic/Extensions/CoreData/NodeInfoEntityToNodeInfo.swift @@ -15,6 +15,7 @@ extension NodeInfoEntity { userProto.shortName = user.shortName ?? "" userProto.hwModel = HardwareModel(rawValue: Int(user.hwModelId)) ?? .unset userProto.isLicensed = user.isLicensed + userProto.isUnmessagable = false userProto.role = Config.DeviceConfig.Role(rawValue: Int(user.role)) ?? .client userProto.publicKey = user.publicKey?.subdata(in: 0..