NFC Tag contact (#1537)

This commit is contained in:
Benjamin Faershtein 2026-01-04 20:38:17 -08:00 committed by GitHub
parent 9ca951b179
commit 5c22b8b6e0
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 196 additions and 1 deletions

View file

@ -10693,6 +10693,10 @@
}
}
},
"Create Node Contact NFC Tag" : {
"comment" : "A section header that instructs the user to create a contact NFC tag.",
"isCommentAutoGenerated" : true
},
"Create Waypoint" : {
"localizations" : {
"de" : {
@ -27747,6 +27751,10 @@
}
}
},
"Node Name: %@" : {
"comment" : "A text label displaying the name of the connected node.",
"isCommentAutoGenerated" : true
},
"Node Number" : {
"localizations" : {
"de" : {
@ -44139,6 +44147,9 @@
}
}
}
},
"Tools" : {
},
"Topic: %@" : {
"localizations" : {
@ -48733,6 +48744,10 @@
}
}
},
"Write Contact to NFC Tag" : {
"comment" : "A button that writes a contact to an NFC tag.",
"isCommentAutoGenerated" : true
},
"x" : {
"localizations" : {
"es" : {
@ -49444,4 +49459,4 @@
}
},
"version" : "1.1"
}
}

View file

@ -97,6 +97,8 @@
<true/>
<key>LSSupportsOpeningDocumentsInPlace</key>
<true/>
<key>NFCReaderUsageDescription</key>
<string>We use NFC tags to share node contacts</string>
<key>NSBluetoothAlwaysUsageDescription</key>
<string>We use bluetooth to connect to nearby Meshtastic Devices</string>
<key>NSBluetoothPeripheralUsageDescription</key>

View file

@ -9,6 +9,10 @@
</array>
<key>com.apple.developer.carplay-communication</key>
<true/>
<key>com.apple.developer.nfc.readersession.formats</key>
<array>
<string>TAG</string>
</array>
<key>com.apple.developer.usernotifications.critical-alerts</key>
<true/>
<key>com.apple.developer.weatherkit</key>

View file

@ -52,6 +52,7 @@ enum SettingsNavigationState: String {
case debugLogs
case appFiles
case firmwareUpdates
case tools
}
struct NavigationState: Hashable {

View file

@ -355,6 +355,13 @@ struct Settings: View {
Image(systemName: "gearshape")
}
}
NavigationLink(value: SettingsNavigationState.tools) {
Label {
Text("Tools")
} icon: {
Image(systemName: "hammer")
}
}
NavigationLink(value: SettingsNavigationState.routes) {
Label {
Text("Routes")
@ -521,6 +528,8 @@ struct Settings: View {
AppData()
case .firmwareUpdates:
Firmware(node: node)
case .tools:
Tools()
}
}
.onChange(of: UserDefaults.preferredPeripheralNum ) { _, newConnectedNode in

View file

@ -0,0 +1,164 @@
//
// Tools.swift
// Meshtastic
//
// Created by Benjamin Faershtein on 12/31/25.
//
import SwiftUI
import CoreNFC
import MeshtasticProtobufs
import OSLog
struct Tools: View {
@EnvironmentObject var accessoryManager: AccessoryManager
@Environment(\.managedObjectContext) var context
@StateObject private var nfcReader = NFCReader()
var connectedNode: NodeInfoEntity? {
if let num = accessoryManager.activeDeviceNum {
return getNodeInfo(id: num, context: context)
}
return nil
}
var qrString: String {
var contact = SharedContact()
contact.nodeNum = UInt32(connectedNode?.num ?? 0)
contact.user = connectedNode?.toProto().user ?? User()
contact.manuallyVerified = true
do {
let contactString = try contact.serializedData().base64EncodedString()
return "https://meshtastic.org/v/#" + contactString.base64ToBase64url()
} catch {
Logger.services.error("Error serializing contact: \(error)")
return ""
}
}
var body: some View {
VStack{
List {
Section(header: Text("Create Node Contact NFC Tag")) {
if let node = connectedNode {
Text("Node Name: \(node.user?.longName ?? "Unknown")")
Button {
nfcReader.scan(theActualData: qrString)
} label: {
Label("Write Contact to NFC Tag", systemImage: "tag")
}
.disabled(qrString.isEmpty)
}
}
}
}
.navigationTitle("Tools")
.navigationBarTitleDisplayMode(.inline)
}
}
#Preview {
Tools()
}
final class NFCReader: NSObject, ObservableObject, NFCNDEFReaderSessionDelegate {
private let logger = Logger(subsystem: "org.meshtastic.app", category: "NFC")
private var payloadString = ""
private var session: NFCNDEFReaderSession?
func scan(theActualData: String) {
payloadString = theActualData
session = NFCNDEFReaderSession(
delegate: self,
queue: nil,
invalidateAfterFirstRead: false
)
session?.alertMessage = "Hold your iPhone near the NFC tag."
session?.begin()
}
func readerSessionDidBecomeActive(_ session: NFCNDEFReaderSession) {
logger.debug("NFC session became active")
}
func readerSession(_ session: NFCNDEFReaderSession,
didInvalidateWithError error: Error) {
logger.error("NFC session invalidated: \(error.localizedDescription)")
}
func readerSession(_ session: NFCNDEFReaderSession,
didDetectNDEFs messages: [NFCNDEFMessage]) {
}
func readerSession(_ session: NFCNDEFReaderSession,
didDetect tags: [NFCNDEFTag]) {
guard tags.count == 1, let tag = tags.first else {
session.alertMessage = "More than one tag detected. Please present only one."
DispatchQueue.global().asyncAfter(deadline: .now() + .milliseconds(500)) {
session.restartPolling()
}
return
}
session.connect(to: tag) { error in
if let error {
self.logger.error("Failed to connect to tag: \(error.localizedDescription)")
session.alertMessage = "Failed to connect to tag."
session.invalidate()
return
}
tag.queryNDEFStatus { status, _, error in
if let error {
self.logger.error("Failed to query NDEF status: \(error.localizedDescription)")
session.alertMessage = "Failed to read tag."
session.invalidate()
return
}
switch status {
case .notSupported:
self.logger.error("Tag does not support NDEF")
session.alertMessage = "Tag does not support NDEF."
session.invalidate()
case .readOnly:
self.logger.error("Tag is read-only")
session.alertMessage = "Tag is read-only."
session.invalidate()
case .readWrite:
guard let payload =
NFCNDEFPayload.wellKnownTypeURIPayload(
string: self.payloadString
) else {
self.logger.error("Invalid NDEF payload")
session.alertMessage = "Invalid payload."
session.invalidate()
return
}
let message = NFCNDEFMessage(records: [payload])
tag.writeNDEF(message) { error in
if let error {
self.logger.error("Failed to write NDEF: \(error.localizedDescription)")
session.alertMessage = "Failed to write tag."
} else {
self.logger.info("Successfully wrote NFC tag")
session.alertMessage = "NFC tag written successfully."
}
session.invalidate()
}
}
}
}
}
}