mirror of
https://github.com/meshtastic/Meshtastic-Apple.git
synced 2026-04-20 22:13:56 +00:00
NFC Tag contact (#1600)
* NFC Tag contact * Add Tools.swift to Xcode project file - fix missing file reference causing build failure Agent-Logs-Url: https://github.com/meshtastic/Meshtastic-Apple/sessions/e3299e28-9ec0-4a23-98bc-5fc032750b4a Co-authored-by: garthvh <1795163+garthvh@users.noreply.github.com> * Apply reviewer feedback: Catalyst guard, NDEF entitlement, nil guard, localized string, capacity check, preview fix Agent-Logs-Url: https://github.com/meshtastic/Meshtastic-Apple/sessions/b86f9b74-5ee1-4144-87e5-3e4b6479ac44 Co-authored-by: garthvh <1795163+garthvh@users.noreply.github.com> * Log tag NDEF capacity on query for debugging Agent-Logs-Url: https://github.com/meshtastic/Meshtastic-Apple/sessions/b86f9b74-5ee1-4144-87e5-3e4b6479ac44 Co-authored-by: garthvh <1795163+garthvh@users.noreply.github.com> * Fix formatting error * Linting fixes --------- Co-authored-by: Benjamin Faershtein <119711889+RCGV1@users.noreply.github.com> Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: garthvh <1795163+garthvh@users.noreply.github.com>
This commit is contained in:
parent
316fc48737
commit
36c07ba685
10 changed files with 228 additions and 7 deletions
|
|
@ -13743,6 +13743,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" : {
|
||||
"da" : {
|
||||
|
|
@ -35452,6 +35456,10 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"Node Name: %@" : {
|
||||
"comment" : "A text label displaying the name of the connected node.",
|
||||
"isCommentAutoGenerated" : true
|
||||
},
|
||||
"Node not heard recently. Shown without a pulsing ring on the map." : {
|
||||
"comment" : "A description of a node that is not heard by the user.",
|
||||
"isCommentAutoGenerated" : true
|
||||
|
|
@ -45333,6 +45341,10 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"RSSI %d dBm" : {
|
||||
"comment" : "A label displaying the RSSI of a message.",
|
||||
"isCommentAutoGenerated" : true
|
||||
},
|
||||
"RSSI %ddB" : {
|
||||
"localizations" : {
|
||||
"da" : {
|
||||
|
|
@ -56280,6 +56292,9 @@
|
|||
"Toggles the map legend" : {
|
||||
"comment" : "A hint for the user to toggle the map legend.",
|
||||
"isCommentAutoGenerated" : true
|
||||
},
|
||||
"Tools" : {
|
||||
|
||||
},
|
||||
"Topic: %@" : {
|
||||
"extractionState" : "stale",
|
||||
|
|
@ -62114,6 +62129,10 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"Write Contact to NFC Tag" : {
|
||||
"comment" : "A button that writes a contact to an NFC tag.",
|
||||
"isCommentAutoGenerated" : true
|
||||
},
|
||||
"x" : {
|
||||
"localizations" : {
|
||||
"da" : {
|
||||
|
|
|
|||
|
|
@ -101,6 +101,7 @@
|
|||
8E587743574CE17703E892C6 /* Certificates in Resources */ = {isa = PBXBuildFile; fileRef = 518D504DED9874EBF9D76578 /* Certificates */; };
|
||||
8EED425B7820DA4FEB40C375 /* CoTXMLParser.swift in Sources */ = {isa = PBXBuildFile; fileRef = 748E4806582595DE80D455CD /* CoTXMLParser.swift */; };
|
||||
9604373EEB96801AA89DF48C /* EXICodec.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3D0A8ABAEF1E587683970927 /* EXICodec.swift */; };
|
||||
DCC919C6B47C15BB0795456C /* Tools.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1D5AD8037A0D583C614B0597 /* Tools.swift */; };
|
||||
A5339E2F74E83F8FC41EEE33 /* TAKServerConfig.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0618E6D0DF90B74EE32E6C06 /* TAKServerConfig.swift */; };
|
||||
AA0001012E2730EC00600001 /* ConnectViewTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA00010022E2730EC0060000 /* ConnectViewTests.swift */; };
|
||||
ABA8E6402E2F2A2300E27791 /* AppIconButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABA8E63F2E2F2A2300E27791 /* AppIconButton.swift */; };
|
||||
|
|
@ -348,6 +349,7 @@
|
|||
|
||||
/* Begin PBXFileReference section */
|
||||
01028778B8BFD81F7A039593 /* TAKConnection.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = TAKConnection.swift; sourceTree = "<group>"; };
|
||||
1D5AD8037A0D583C614B0597 /* Tools.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Tools.swift; sourceTree = "<group>"; };
|
||||
0618E6D0DF90B74EE32E6C06 /* TAKServerConfig.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = TAKServerConfig.swift; sourceTree = "<group>"; };
|
||||
09936BEBD6D82479B2360FDC /* TAKCertificateManager.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = TAKCertificateManager.swift; sourceTree = "<group>"; };
|
||||
108FFECA2DD3F43C00BFAA81 /* ShareContactQRDialog.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShareContactQRDialog.swift; sourceTree = "<group>"; };
|
||||
|
|
@ -1034,6 +1036,7 @@
|
|||
DDCE4E2B2869F92900BE9F8F /* UserConfig.swift */,
|
||||
ABA8E63F2E2F2A2300E27791 /* AppIconButton.swift */,
|
||||
0618E6D0DF90B74EE32E6C06 /* TAKServerConfig.swift */,
|
||||
1D5AD8037A0D583C614B0597 /* Tools.swift */,
|
||||
);
|
||||
path = Settings;
|
||||
sourceTree = "<group>";
|
||||
|
|
@ -1942,6 +1945,7 @@
|
|||
E3ED80145D0E873011982556 /* TAKServerManager.swift in Sources */,
|
||||
FE508F9AF5AD5DA20AA64DBF /* AccessoryManager+TAK.swift in Sources */,
|
||||
A5339E2F74E83F8FC41EEE33 /* TAKServerConfig.swift in Sources */,
|
||||
DCC919C6B47C15BB0795456C /* Tools.swift in Sources */,
|
||||
8398407DBA32EE7CFC16A385 /* TAKDataPackageGenerator.swift in Sources */,
|
||||
655AF7816E76D5F310DF87A6 /* FountainCodec.swift in Sources */,
|
||||
9604373EEB96801AA89DF48C /* EXICodec.swift in Sources */,
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -9,6 +9,10 @@
|
|||
</array>
|
||||
<key>com.apple.developer.carplay-communication</key>
|
||||
<true/>
|
||||
<key>com.apple.developer.nfc.readersession.formats</key>
|
||||
<array>
|
||||
<string>NDEF</string>
|
||||
</array>
|
||||
<key>com.apple.developer.usernotifications.critical-alerts</key>
|
||||
<true/>
|
||||
<key>com.apple.developer.weatherkit</key>
|
||||
|
|
|
|||
|
|
@ -53,6 +53,7 @@ enum SettingsNavigationState: String {
|
|||
case appFiles
|
||||
case firmwareUpdates
|
||||
case tak
|
||||
case tools
|
||||
}
|
||||
|
||||
struct NavigationState: Hashable {
|
||||
|
|
|
|||
|
|
@ -97,7 +97,7 @@ struct MessageContextMenuItems: View {
|
|||
if !isCurrentUser && !(message.fromUser?.userNode?.viaMqtt ?? false) && message.fromUser?.userNode?.hopsAway ?? -1 == 0 {
|
||||
VStack {
|
||||
Text("SNR \(String(format: "%.2f", message.snr)) dB")
|
||||
Text("RSSI \(String(format: "%.2f", message.rssi)) dBm")
|
||||
Text("RSSI \(message.rssi) dBm")
|
||||
}
|
||||
} else if !isCurrentUser && !(message.fromUser?.userNode?.viaMqtt ?? false) {
|
||||
VStack {
|
||||
|
|
|
|||
|
|
@ -32,9 +32,8 @@ struct WaypointForm: View {
|
|||
@State private var lockedTo: Int64 = 0
|
||||
@State private var selectedDetent: PresentationDetent = .medium
|
||||
@State private var waypointFailedAlert: Bool = false
|
||||
@State private var createdByNode : NodeInfoEntity? = nil
|
||||
@State private var lastUpdatedByNode : NodeInfoEntity? = nil
|
||||
|
||||
@State private var createdByNode: NodeInfoEntity? = nil
|
||||
@State private var lastUpdatedByNode: NodeInfoEntity? = nil
|
||||
|
||||
var body: some View {
|
||||
Group {
|
||||
|
|
@ -530,5 +529,3 @@ struct WaypointForm: View {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -35,7 +35,6 @@ struct RangeTestConfig: View {
|
|||
return hexLen < 3
|
||||
}
|
||||
|
||||
|
||||
var body: some View {
|
||||
Form {
|
||||
ConfigHeader(title: "Range", config: \.rangeTestConfig, node: node, onAppear: setRangeTestValues)
|
||||
|
|
|
|||
|
|
@ -367,6 +367,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")
|
||||
|
|
@ -534,6 +541,8 @@ struct Settings: View {
|
|||
AppData()
|
||||
case .firmwareUpdates:
|
||||
Firmware(node: node)
|
||||
case .tools:
|
||||
Tools()
|
||||
case .tak:
|
||||
TAKServerConfig()
|
||||
}
|
||||
|
|
|
|||
186
Meshtastic/Views/Settings/Tools.swift
Normal file
186
Meshtastic/Views/Settings/Tools.swift
Normal file
|
|
@ -0,0 +1,186 @@
|
|||
//
|
||||
// Tools.swift
|
||||
// Meshtastic
|
||||
//
|
||||
// Created by Benjamin Faershtein on 12/31/25.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
#if !targetEnvironment(macCatalyst)
|
||||
import CoreNFC
|
||||
#endif
|
||||
import MeshtasticProtobufs
|
||||
import OSLog
|
||||
|
||||
struct Tools: View {
|
||||
@EnvironmentObject var accessoryManager: AccessoryManager
|
||||
@Environment(\.managedObjectContext) var context
|
||||
|
||||
#if !targetEnvironment(macCatalyst)
|
||||
@StateObject private var nfcReader = NFCReader()
|
||||
#endif
|
||||
|
||||
var connectedNode: NodeInfoEntity? {
|
||||
if let num = accessoryManager.activeDeviceNum {
|
||||
return getNodeInfo(id: num, context: context)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
var qrString: String {
|
||||
guard let connectedNode = connectedNode else {
|
||||
return ""
|
||||
}
|
||||
|
||||
var contact = SharedContact()
|
||||
contact.nodeNum = UInt32(connectedNode.num)
|
||||
contact.user = connectedNode.toProto().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".localized)")
|
||||
#if !targetEnvironment(macCatalyst)
|
||||
Button {
|
||||
nfcReader.scan(theActualData: qrString)
|
||||
} label: {
|
||||
Label("Write Contact to NFC Tag", systemImage: "tag")
|
||||
}
|
||||
.disabled(qrString.isEmpty)
|
||||
#endif
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.navigationTitle("Tools")
|
||||
.navigationBarTitleDisplayMode(.inline)
|
||||
}
|
||||
}
|
||||
|
||||
#Preview {
|
||||
let context = PersistenceController.preview.container.viewContext
|
||||
return Tools()
|
||||
.environmentObject(AccessoryManager.shared)
|
||||
.environment(\.managedObjectContext, context)
|
||||
}
|
||||
|
||||
#if !targetEnvironment(macCatalyst)
|
||||
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, capacity, error in
|
||||
if let error {
|
||||
self.logger.error("Failed to query NDEF status: \(error.localizedDescription)")
|
||||
session.alertMessage = "Failed to read tag."
|
||||
session.invalidate()
|
||||
return
|
||||
}
|
||||
self.logger.debug("Tag NDEF status: \(String(describing: status)), capacity: \(capacity) bytes")
|
||||
|
||||
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])
|
||||
|
||||
guard message.length <= capacity else {
|
||||
self.logger.error("Payload (\(message.length) bytes) exceeds tag capacity (\(capacity) bytes)")
|
||||
session.alertMessage = "Tag too small to hold contact data."
|
||||
session.invalidate()
|
||||
return
|
||||
}
|
||||
|
||||
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()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
||||
Loading…
Add table
Add a link
Reference in a new issue