Merge pull request #1225 from meshtastic/add-share-contact

Add share contact via QR (and untested scanning of contact QR codes)
This commit is contained in:
Garth Vander Houwen 2025-05-14 20:13:27 -07:00 committed by GitHub
commit b47ce37bcf
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
15 changed files with 465 additions and 49 deletions

View file

@ -7202,6 +7202,16 @@
}
}
},
"Contact URL" : {
"localizations" : {
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "Contact URL"
}
}
}
},
"Contacts (%@)" : {
"localizations" : {
"de" : {
@ -9994,6 +10004,9 @@
}
}
}
},
"Done" : {
},
"Double Tap as Button" : {
"localizations" : {
@ -15082,6 +15095,12 @@
}
}
}
},
"Add Contact" : {
},
"Add Meshtastic Node %@ as a contact" : {
},
"Import Route" : {
"localizations" : {
@ -22193,6 +22212,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 +22532,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 +26040,9 @@
}
}
}
},
"Scan this QR code to add %@ to another device." : {
},
"Screen on for" : {
"localizations" : {
@ -28033,6 +28055,9 @@
}
}
}
},
"Share Contact QR" : {
},
"Share QR Code" : {
"localizations" : {
@ -29711,6 +29736,9 @@
}
}
}
},
"Takes a Meshtastic contact URL and saves it to the nodes database" : {
},
"Tapback" : {
"localizations" : {
@ -30891,6 +30919,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" : {

View file

@ -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 */; };
@ -56,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 */; };
@ -274,6 +277,8 @@
/* End PBXCopyFilesBuildPhase section */
/* Begin PBXFileReference section */
108FFECA2DD3F43C00BFAA81 /* ShareContactQRDialog.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShareContactQRDialog.swift; sourceTree = "<group>"; };
108FFECC2DD4005600BFAA81 /* NodeInfoEntityToNodeInfo.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NodeInfoEntityToNodeInfo.swift; sourceTree = "<group>"; };
231B3F1F2D087A4C0069A07D /* MetricsColumnList.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MetricsColumnList.swift; sourceTree = "<group>"; };
231B3F202D087A4C0069A07D /* MetricTableColumn.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MetricTableColumn.swift; sourceTree = "<group>"; };
231B3F242D087C3C0069A07D /* EnvironmentDefaultColumns.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EnvironmentDefaultColumns.swift; sourceTree = "<group>"; };
@ -318,6 +323,7 @@
8D3F8A402D44C2A6009EAAA4 /* PowerMetricsLog.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PowerMetricsLog.swift; sourceTree = "<group>"; };
B399E8A32B6F486400E4488E /* RetryButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RetryButton.swift; sourceTree = "<group>"; };
B3E905B02B71F7F300654D07 /* TextMessageField.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TextMessageField.swift; sourceTree = "<group>"; };
BC10380E2DD4333C00B00BFA /* AddContactIntent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddContactIntent.swift; sourceTree = "<group>"; };
BC47C2EE2CE0017D008245CA /* MessageNodeIntent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageNodeIntent.swift; sourceTree = "<group>"; };
BC5EBA3B2D002A2000C442FF /* MessageNodeIntent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageNodeIntent.swift; sourceTree = "<group>"; };
BC6B45FE2CB2F98900723CEB /* SaveChannelSettingsIntent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SaveChannelSettingsIntent.swift; sourceTree = "<group>"; };
@ -672,6 +678,7 @@
BCB6137F2C6728E700485544 /* AppIntents */ = {
isa = PBXGroup;
children = (
BC10380E2DD4333C00B00BFA /* AddContactIntent.swift */,
BC5EBA3B2D002A2000C442FF /* MessageNodeIntent.swift */,
BCB613802C67290800485544 /* SendWaypointIntent.swift */,
BCB613822C672A2600485544 /* MessageChannelIntent.swift */,
@ -701,6 +708,7 @@
DD007BB12AA59B9A00F5FA12 /* CoreData */ = {
isa = PBXGroup;
children = (
108FFECC2DD4005600BFAA81 /* NodeInfoEntityToNodeInfo.swift */,
2344A2AA2D66973D00170A77 /* ManagedAttributePropertyWrapper.swift */,
DD58C5F12919AD3C00D5BEFB /* ChannelEntityExtension.swift */,
6DEDA55B2A9592F900321D2E /* MessageEntityExtension.swift */,
@ -1101,6 +1109,7 @@
DDDB26402AABEF7B003AFCB7 /* Helpers */ = {
isa = PBXGroup;
children = (
108FFECA2DD3F43C00BFAA81 /* ShareContactQRDialog.swift */,
231B3F232D087C020069A07D /* Metrics Columns */,
DDAD49EB2AFAE82500B4425D /* Map */,
DDDB26432AAC0206003AFCB7 /* NodeDetail.swift */,
@ -1396,6 +1405,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 +1496,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 */,
@ -1543,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 */,

View file

@ -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 = "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 add")
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 add 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")
}
}
}

View file

@ -0,0 +1,28 @@
// 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)) ?? .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..<user.publicKey!.count) ?? Data()
}
var node = NodeInfo()
node.num = UInt32(self.num)
node.user = userProto
// Add more fields as needed
return node
}
}

View file

@ -1770,6 +1770,56 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate
return false
}
public func addContactFromURL(base64UrlString: String) -> 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)..<UInt32.max)
meshPacket.priority = MeshPacket.Priority.reliable
meshPacket.wantAck = true
meshPacket.channel = 0
var dataMessage = DataMessage()
guard let adminData: Data = try? adminPacket.serializedData() else {
return false
}
dataMessage.payload = adminData
dataMessage.portnum = PortNum.adminApp
meshPacket.decoded = dataMessage
var toRadio: ToRadio!
toRadio = ToRadio()
toRadio.packet = meshPacket
guard let binaryData: Data = try? toRadio.serializedData() else {
return false
}
if connectedPeripheral?.peripheral.state ?? CBPeripheralState.disconnected == CBPeripheralState.connected {
self.connectedPeripheral.peripheral.writeValue(binaryData, for: self.TORADIO_characteristic, type: .withResponse)
let logString = String.localizedStringWithFormat("Sent a LoRa.Config for: %@".localized, String(connectedPeripheral.num))
Logger.mesh.info("📻 \(logString, privacy: .public)")
}
if self.connectedPeripheral != nil {
self.sendWantConfig()
return true
}
} catch {
Logger.data.error("Failed to decode contact data: \(error.localizedDescription, privacy: .public)")
return false
}
}
}
return false
}
public func saveUser(config: User, fromUser: UserEntity, toUser: UserEntity, adminIndex: Int32) -> Int64 {
var adminPacket = AdminMessage()
adminPacket.setOwner = config

View file

@ -7,6 +7,7 @@
<key>com.apple.developer.associated-domains</key>
<array>
<string>applinks:meshtastic.org/e/*</string>
<string>applinks:meshtastic.org/v/*</string>
</array>
<key>com.apple.developer.weatherkit</key>
<true/>

View file

@ -4,6 +4,7 @@ import SwiftUI
import CoreData
import OSLog
import TipKit
import MeshtasticProtobufs
@main
struct MeshtasticAppleApp: App {
@ -87,7 +88,62 @@ 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/#") {
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 +175,7 @@ struct MeshtasticAppleApp: App {
.displayFrequency(.immediate)
]
)
}
}
}
.onChange(of: scenePhase) { (_, newScenePhase) in
switch newScenePhase {

View file

@ -0,0 +1,97 @@
// 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 ""
}
}
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()
.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)
ShareLink("Share QR Code & Link",
item: Image(uiImage: qrImage),
subject: Text("Add Meshtastic Node \(node.user.shortName) as a contact"),
message: Text(qrString),
preview: SharePreview("Add Meshtastic Node \(node.user.shortName) as a contact",
image: Image(uiImage: qrImage))
)
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

View file

@ -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,13 @@ struct NodeList: View {
}
}
}
}
.sheet(isPresented: $isPresentingShareContactQR) {
if let node = shareContactNode {
ShareContactQRDialog(node: node.toProto())
} else {
EmptyView()
}
}
.navigationSplitViewColumnWidth(min: 100, ideal: 250, max: 500)
.navigationBarItems(

View file

@ -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<D: SwiftProtobuf.Decoder>(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<V: SwiftProtobuf.Visitor>(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
}

View file

@ -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<D: SwiftProtobuf.Decoder>(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<V: SwiftProtobuf.Visitor>(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
}

View file

@ -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<D: SwiftProtobuf.Decoder>(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
}

View file

@ -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<D: SwiftProtobuf.Decoder>(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
}

View file

@ -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"),
]
}

@ -1 +1 @@
Subproject commit 816595c8bbdfc3b4388e11348ccd043294d58705
Subproject commit 47ec99aa4c4a2e3fff71fd5170663f0848deb021