Clean up channel qr code functionality.

This commit is contained in:
Garth Vander Houwen 2025-11-01 08:29:47 -07:00
parent 0fcf4fdbcb
commit b4c749a978
5 changed files with 94 additions and 70 deletions

View file

@ -36770,7 +36770,12 @@
}
}
},
"These settings will %@" : {
"comment" : "A paragraph below the title that explains what the user is about to do.",
"isCommentAutoGenerated" : true
},
"These settings will %@ channels. The current LoRa Config will be replaced, if there are substantial changes to the LoRa config the device will reboot" : {
"extractionState" : "stale",
"localizations" : {
"it" : {
"stringUnit" : {

View file

@ -509,32 +509,32 @@ extension AccessoryManager {
let logString = String.localizedStringWithFormat("Sent a Channel for: %@ Channel Index %d".localized, String(deviceNum), chan.index)
try await send(toRadio, debugDescription: logString)
}
// Save the LoRa Config and the device will reboot
var adminPacket = AdminMessage()
adminPacket.setConfig.lora = channelSet.loraConfig
adminPacket.setConfig.lora.configOkToMqtt = okToMQTT // Preserve users okToMQTT choice
var meshPacket: MeshPacket = MeshPacket()
meshPacket.to = UInt32(deviceNum)
meshPacket.from = UInt32(deviceNum)
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 {
throw AccessoryError.ioFailed("sendReboot: Unable to serialize Admin packet")
if !addChannels {
// Save the LoRa Config and the device will reboot
var adminPacket = AdminMessage()
adminPacket.setConfig.lora = channelSet.loraConfig
adminPacket.setConfig.lora.configOkToMqtt = okToMQTT // Preserve users okToMQTT choice
var meshPacket: MeshPacket = MeshPacket()
meshPacket.to = UInt32(deviceNum)
meshPacket.from = UInt32(deviceNum)
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 {
throw AccessoryError.ioFailed("sendReboot: Unable to serialize Admin packet")
}
dataMessage.payload = adminData
dataMessage.portnum = PortNum.adminApp
meshPacket.decoded = dataMessage
var toRadio: ToRadio!
toRadio = ToRadio()
toRadio.packet = meshPacket
let logString = String.localizedStringWithFormat("Sent a LoRa.Config for: %@".localized, String(deviceNum))
try await send(toRadio, debugDescription: logString)
}
dataMessage.payload = adminData
dataMessage.portnum = PortNum.adminApp
meshPacket.decoded = dataMessage
var toRadio: ToRadio!
toRadio = ToRadio()
toRadio.packet = meshPacket
let logString = String.localizedStringWithFormat("Sent a LoRa.Config for: %@".localized, String(deviceNum))
try await send(toRadio, debugDescription: logString)
Logger.transport.debug("[AccessoryManager] sending wantConfig for saveChannelSet")
try await sendWantConfig()
}

View file

@ -31,6 +31,18 @@ extension URL {
return nil
}
}
var queryParameters: [String: String]? {
guard let components = URLComponents(url: self, resolvingAgainstBaseURL: true),
let queryItems = components.queryItems else {
return nil
}
var parameters = [String: String]()
for item in queryItems {
parameters[item.name] = item.value
}
return parameters
}
var attributes: [FileAttributeKey: Any]? {
do {
return try FileManager.default.attributesOfItem(atPath: path)

View file

@ -11,6 +11,7 @@ import DatadogRUM
import DatadogTrace
import DatadogLogs
import DatadogSessionReplay
@main
struct MeshtasticAppleApp: App {
@ -55,7 +56,7 @@ struct MeshtasticAppleApp: App {
Logs.enable()
Trace.enable(
with: Trace.Configuration(
sampleRate: 100, networkInfoEnabled: true // 100% sampling for development/testing, reduce for production
sampleRate: 100, networkInfoEnabled: true // 100% sampling for development/testing, reduce for production
)
)
@ -96,14 +97,45 @@ struct MeshtasticAppleApp: App {
#endif
if !UserDefaults.firstLaunch {
// If this is first launch, we will show onboarding screens which
// Step through the authorization process. Do not start discovery
// Step through the authorization process. Do not start discovery
// unitl this workflow completes, otherwise the discovery process
// may trigger permission dialogs too soon.
accessoryManager.startDiscovery()
}
}
var body: some Scene {
WindowGroup {
private func handleChannelLinkURL(_ url: URL, fromActivity: Bool) {
// Reset the state before processing a new URL
self.saveChannelLink = nil
guard url.absoluteString.lowercased().contains("meshtastic.org/e/") else {
return
}
let queryParams = url.queryParameters
let addChannels = Bool(queryParams?["add"] ?? "false") ?? false
var channelData: String?
let urlString = url.absoluteString
if let fragment = urlString.components(separatedBy: "#").last, !fragment.isEmpty {
channelData = fragment.components(separatedBy: "?").first
}
guard let finalChannelData = channelData, !finalChannelData.isEmpty else {
Logger.mesh.error("Could not extract channel data from URL: \(url.absoluteString, privacy: .public)")
return
}
self.saveChannelLink = SaveChannelLinkData(data: finalChannelData, add: addChannels)
Logger.services.debug("Add Channel \(addChannels, privacy: .public) with data: \(finalChannelData, privacy: .public)")
// Log based on the calling context
let source = fromActivity ? "User Activity" : "Open URL"
Logger.mesh.debug("User wants to open a Channel Settings URL (\(source)): \(url.absoluteString, privacy: .public)")
}
var body: some Scene {
WindowGroup {
ContentView(
appState: appState,
router: appState.router
@ -112,7 +144,7 @@ struct MeshtasticAppleApp: App {
) { link in
SaveChannelQRCode(
channelSetLink: link.data,
addChannels: link.add,
addChannels: link.add, // <-- Uses the now reliable 'add' boolean
accessoryManager: accessoryManager )
.presentationDetents([.large])
.presentationDragIndicator(.visible)
@ -121,27 +153,16 @@ struct MeshtasticAppleApp: App {
Logger.mesh.debug("URL received \(userActivity, privacy: .public)")
self.incomingUrl = userActivity.webpageURL
self.saveChannelLink = nil
var addChannels = false
if self.incomingUrl?.absoluteString.lowercased().contains("meshtastic.org/v/#") == true {
ContactURLHandler.handleContactUrl(url: self.incomingUrl!, accessoryManager: accessoryManager)
} else if self.incomingUrl?.absoluteString.lowercased().contains("meshtastic.org/e/") == true {
if let components = self.incomingUrl?.absoluteString.components(separatedBy: "#") {
addChannels = Bool(self.incomingUrl?["add"] ?? "false") ?? false
if (self.incomingUrl?.absoluteString.lowercased().contains("?")) != nil {
guard let cs = components.last!.components(separatedBy: "?").first else {
return
}
self.saveChannelLink = SaveChannelLinkData(data: cs, add: addChannels)
} else {
guard let cs = components.first else {
return
}
self.saveChannelLink = SaveChannelLinkData(data: cs, add: addChannels)
}
Logger.services.debug("Add Channel \(addChannels, privacy: .public)")
if let url = userActivity.webpageURL {
if url.absoluteString.lowercased().contains("meshtastic.org/v/#") == true {
ContactURLHandler.handleContactUrl(url: url, accessoryManager: accessoryManager)
} else if url.absoluteString.lowercased().contains("meshtastic.org/e/") == true {
// **Consolidated Call for User Activity**
handleChannelLinkURL(url, fromActivity: true)
}
Logger.mesh.debug("User wants to open a Channel Settings URL: \(self.incomingUrl?.absoluteString ?? "No QR Code Link")")
}
if self.saveChannelLink != nil {
Logger.mesh.debug("User wants to open Channel Settings URL: \(String(describing: self.incomingUrl!.relativeString), privacy: .public)")
}
@ -149,26 +170,12 @@ struct MeshtasticAppleApp: App {
.onOpenURL(perform: { (url) in
Logger.mesh.debug("Some sort of URL was received \(url, privacy: .public)")
self.incomingUrl = url
var addChannels = false
if url.absoluteString.lowercased().contains("meshtastic.org/v/#") {
ContactURLHandler.handleContactUrl(url: url, accessoryManager: accessoryManager)
} else if url.absoluteString.lowercased().contains("meshtastic.org/e/") {
if let components = self.incomingUrl?.absoluteString.components(separatedBy: "#") {
addChannels = Bool(self.incomingUrl?["add"] ?? "false") ?? false
if self.incomingUrl?.absoluteString.lowercased().contains("?") != nil {
guard let cs = components.last!.components(separatedBy: "?").first else {
return
}
self.saveChannelLink = SaveChannelLinkData(data: cs, add: addChannels)
} else {
guard let cs = components.first else {
return
}
self.saveChannelLink = SaveChannelLinkData(data: cs, add: addChannels)
}
Logger.services.debug("Add Channel \(addChannels, privacy: .public)")
}
Logger.mesh.debug("User wants to open a Channel Settings URL: \(self.incomingUrl?.absoluteString ?? "No QR Code Link", privacy: .public)")
// **Consolidated Call for Open URL**
handleChannelLinkURL(url, fromActivity: false)
} else if url.absoluteString.lowercased().contains("meshtastic:///") {
appState.router.route(url: url)
}
@ -183,7 +190,7 @@ struct MeshtasticAppleApp: App {
.displayFrequency(.immediate)
]
)
}
}
}
.onChange(of: scenePhase) { (_, newScenePhase) in
switch newScenePhase {

View file

@ -19,7 +19,7 @@ struct SaveChannelQRCode: View {
@Environment(\.dismiss) private var dismiss
@Environment(\.managedObjectContext) var context
let channelSetLink: String
var addChannels: Bool = false
@State var addChannels: Bool = false
var accessoryManager: AccessoryManager
@State private var showError: Bool = false
@ -31,13 +31,13 @@ struct SaveChannelQRCode: View {
VStack {
Text("\(addChannels ? "Add" : "Replace all") Channels?")
.font(.title)
Text("These settings will \(addChannels ? "add" : "replace all") channels. The current LoRa Config will be replaced, if there are substantial changes to the LoRa config the device will reboot")
Text("These settings will \(addChannels ? "add channels without changing any LoRa config values." : "replace all channels. The current LoRa Config will be replaced, if there are substantial changes to the LoRa config the device will reboot automatically.")")
.fixedSize(horizontal: false, vertical: true)
.foregroundColor(.gray)
.font(.title3)
.padding()
if !loraChanges.isEmpty {
if !loraChanges.isEmpty && !addChannels {
VStack(alignment: .leading) {
Text("LoRa Config Changes:")
.font(.headline)