diff --git a/Meshtastic.xcodeproj/project.pbxproj b/Meshtastic.xcodeproj/project.pbxproj index fc609027..37a37ae1 100644 --- a/Meshtastic.xcodeproj/project.pbxproj +++ b/Meshtastic.xcodeproj/project.pbxproj @@ -847,7 +847,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 1.3.21; + MARKETING_VERSION = 1.3.22; PRODUCT_BUNDLE_IDENTIFIER = gvh.MeshtasticClient; PRODUCT_NAME = "$(TARGET_NAME)"; SUPPORTS_MACCATALYST = YES; @@ -879,7 +879,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 1.3.21; + MARKETING_VERSION = 1.3.22; PRODUCT_BUNDLE_IDENTIFIER = gvh.MeshtasticClient; PRODUCT_NAME = "$(TARGET_NAME)"; SUPPORTS_MACCATALYST = YES; diff --git a/Meshtastic/Helpers/BLEManager.swift b/Meshtastic/Helpers/BLEManager.swift index 38615180..c7e02928 100644 --- a/Meshtastic/Helpers/BLEManager.swift +++ b/Meshtastic/Helpers/BLEManager.swift @@ -55,13 +55,13 @@ class BLEManager: NSObject, ObservableObject, CBCentralManagerDelegate, CBPeriph let FROMRADIO_UUID = CBUUID(string: "0x8BA2BCC2-EE02-4A55-A531-C525C5E454D5") let FROMNUM_UUID = CBUUID(string: "0xED9DA18C-A800-4F66-A670-AA7547E34453") - private var meshLoggingEnabled: Bool = false + private var meshLoggingEnabled: Bool = true let meshLog = documentsFolder.appendingPathComponent("meshlog.txt") // MARK: init BLEManager override init() { - self.meshLoggingEnabled = UserDefaults.standard.object(forKey: "meshActivityLog") as? Bool ?? false + //self.meshLoggingEnabled = UserDefaults.standard.object(forKey: "meshActivityLog") as? Bool ?? false self.lastConnectionError = "" self.lastConnnectionVersion = "0.0.0" super.init() @@ -443,10 +443,17 @@ class BLEManager: NSObject, ObservableObject, CBCentralManagerDelegate, CBPeriph localConfig(config: decodedInfo.config, meshlogging: meshLoggingEnabled, context: context!, nodeNum: self.connectedPeripheral.num, nodeLongName: self.connectedPeripheral.longName) + } else if decodedInfo.moduleConfig.isInitialized { + + moduleConfig(config: decodedInfo.moduleConfig, meshlogging: meshLoggingEnabled, context: context!, nodeNum: self.connectedPeripheral.num, nodeLongName: self.connectedPeripheral.longName) + } else { if decodedInfo.configCompleteID == 0 { + if meshLoggingEnabled { MeshLogger.log("ℹ️ MESH PACKET received for App UNHANDLED \(try! decodedInfo.packet.jsonString())") } + } else { + if meshLoggingEnabled { MeshLogger.log("ℹ️ MESH PACKET received for Unknown App UNHANDLED \(try! decodedInfo.packet.jsonString())") } } } @@ -1038,10 +1045,12 @@ class BLEManager: NSObject, ObservableObject, CBCentralManagerDelegate, CBPeriph return false } - public func getModuleConfig (configType: AdminMessage.ModuleConfigType, destNum: Int64, wantResponse: Bool) -> Bool { + public func getChannelSet (destNum: Int64, wantResponse: Bool) -> Bool { var adminPacket = AdminMessage() - adminPacket.getModuleConfigRequest = configType + adminPacket.getChannelRequest = 1 + + var meshPacket: MeshPacket = MeshPacket() meshPacket.to = UInt32(connectedPeripheral.num) diff --git a/Meshtastic/Helpers/MeshPackets.swift b/Meshtastic/Helpers/MeshPackets.swift index 54e2adef..9310b265 100644 --- a/Meshtastic/Helpers/MeshPackets.swift +++ b/Meshtastic/Helpers/MeshPackets.swift @@ -328,9 +328,81 @@ func localConfig (config: Config, meshlogging: Bool, context:NSManagedObjectCont } } -func moduleConfig (config: ModuleConfig, meshlogging: Bool, context:NSManagedObject, nodeNum: Int64, nodeLongName: String) { - +func moduleConfig (config: ModuleConfig, meshlogging: Bool, context:NSManagedObjectContext, nodeNum: Int64, nodeLongName: String) { + // We don't care about any of the WiFi related MQTT settings + if config.payloadVariant == ModuleConfig.OneOf_PayloadVariant.rangeTest(config.rangeTest) { + + var isDefault = false + + if (try! config.rangeTest.jsonString()) == "{}" { + + isDefault = true + print("⛰️ Default Range Test Module config") + } + + let fetchNodeInfoRequest: NSFetchRequest = NSFetchRequest.init(entityName: "NodeInfoEntity") + fetchNodeInfoRequest.predicate = NSPredicate(format: "num == %lld", Int64(nodeNum)) + + do { + + let fetchedNode = try context.fetch(fetchNodeInfoRequest) as! [NodeInfoEntity] + // Found a node, save Device Config + if !fetchedNode.isEmpty { + + if fetchedNode[0].rangeTestConfig == nil { + + let newRangeTestConfig = RangeTestConfigEntity(context: context) + + if isDefault { + + newRangeTestConfig.sender = 0 + newRangeTestConfig.enabled = false + newRangeTestConfig.save = false + + } else { + + newRangeTestConfig.sender = Int32(config.rangeTest.sender) + newRangeTestConfig.enabled = !config.rangeTest.enabled + newRangeTestConfig.save = config.rangeTest.save + } + + fetchedNode[0].rangeTestConfig = newRangeTestConfig + + } else { + + if isDefault { + + fetchedNode[0].rangeTestConfig?.sender = 0 + fetchedNode[0].rangeTestConfig?.enabled = false + fetchedNode[0].rangeTestConfig?.save = false + + } else { + // Client default protobuf value of 0 + fetchedNode[0].rangeTestConfig?.sender = Int32(config.rangeTest.sender) + fetchedNode[0].rangeTestConfig?.enabled = !config.rangeTest.enabled + fetchedNode[0].rangeTestConfig?.save = config.rangeTest.save + } + } + + do { + + try context.save() + if meshlogging { MeshLogger.log("💾 Updated Range Test Config for node number: \(String(nodeNum))") } + + } catch { + + context.rollback() + + let nsError = error as NSError + print("💥 Error Updating Core Data RangeTestConfigEntity: \(nsError)") + } + } + + } catch { + + } + } } func myInfoPacket (myInfo: MyNodeInfo, meshLogging: Bool, context: NSManagedObjectContext) -> MyInfoEntity? { @@ -782,16 +854,20 @@ func routingPacket (packet: MeshPacket, meshLogging: Bool, context: NSManagedObj do { - let fetchedMessage = try context.fetch(fetchMessageRequest)[0] as? MessageEntity + let fetchedMessage = try context.fetch(fetchMessageRequest) as? [MessageEntity] - if fetchedMessage != nil { + if fetchedMessage?.count ?? 0 > 0 { - fetchedMessage!.receivedACK = true - fetchedMessage!.ackSNR = packet.rxSnr - fetchedMessage!.ackTimestamp = Int32(packet.rxTime) - fetchedMessage!.objectWillChange.send() - fetchedMessage!.fromUser?.objectWillChange.send() - fetchedMessage!.toUser?.objectWillChange.send() + fetchedMessage![0].receivedACK = true + fetchedMessage![0].ackSNR = packet.rxSnr + fetchedMessage![0].ackTimestamp = Int32(packet.rxTime) + fetchedMessage![0].objectWillChange.send() + fetchedMessage![0].fromUser?.objectWillChange.send() + fetchedMessage![0].toUser?.objectWillChange.send() + + } else { + + return } try context.save() diff --git a/Meshtastic/Meshtastic.entitlements b/Meshtastic/Meshtastic.entitlements index 3d8c755d..5c8b6ee4 100644 --- a/Meshtastic/Meshtastic.entitlements +++ b/Meshtastic/Meshtastic.entitlements @@ -4,7 +4,7 @@ com.apple.developer.associated-domains - applinks:meshtastic.org/* + applinks:meshtastic.org/e/* com.apple.security.app-sandbox diff --git a/Meshtastic/MeshtasticApp.swift b/Meshtastic/MeshtasticApp.swift index 89be5a4d..bdcdf610 100644 --- a/Meshtastic/MeshtasticApp.swift +++ b/Meshtastic/MeshtasticApp.swift @@ -20,21 +20,17 @@ struct MeshtasticAppleApp: App { .environmentObject(bleManager) .environmentObject(userSettings) .onContinueUserActivity(NSUserActivityTypeBrowsingWeb) { userActivity in - - print("Continue activity \(userActivity)") + + print("QR Code URL received from the Camera \(userActivity)") guard let url = userActivity.webpageURL else { return } - + print("User wants to open URL: \(url)") - // TODO same handling as done in onOpenURL() } .onOpenURL(perform: { (url) in - print("URL OPENED") - print(url) - //we are expecting a .mbtiles map file that contains raster data //save it to the documents directory, and name it offline_map.mbtiles let fileManager = FileManager.default diff --git a/Meshtastic/Model/UserSettings.swift b/Meshtastic/Model/UserSettings.swift index c3d82509..99d92220 100644 --- a/Meshtastic/Model/UserSettings.swift +++ b/Meshtastic/Model/UserSettings.swift @@ -63,7 +63,7 @@ class UserSettings: ObservableObject { self.provideLocation = UserDefaults.standard.object(forKey: "provideLocation") as? Bool ?? false self.provideLocationInterval = UserDefaults.standard.object(forKey: "provideLocationInterval") as? Int ?? 900 self.keyboardType = UserDefaults.standard.object(forKey: "keyboardType") as? Int ?? 0 - self.meshActivityLog = UserDefaults.standard.object(forKey: "meshActivityLog") as? Bool ?? false + self.meshActivityLog = UserDefaults.standard.object(forKey: "meshActivityLog") as? Bool ?? true self.meshMapType = UserDefaults.standard.string(forKey: "meshMapType") ?? "hybrid" self.meshMapCustomTileServer = UserDefaults.standard.string(forKey: "meshMapCustomTileServer") ?? "" } diff --git a/Meshtastic/Protobufs/mesh.pb.swift b/Meshtastic/Protobufs/mesh.pb.swift index b1f4484d..a09549de 100644 --- a/Meshtastic/Protobufs/mesh.pb.swift +++ b/Meshtastic/Protobufs/mesh.pb.swift @@ -1855,6 +1855,16 @@ struct FromRadio { set {_uniqueStorage()._payloadVariant = .rebooted(newValue)} } + /// + /// Include module config + var moduleConfig: ModuleConfig { + get { + if case .moduleConfig(let v)? = _storage._payloadVariant {return v} + return ModuleConfig() + } + set {_uniqueStorage()._payloadVariant = .moduleConfig(newValue)} + } + var unknownFields = SwiftProtobuf.UnknownStorage() /// @@ -1889,6 +1899,9 @@ struct FromRadio { /// Not used on all transports, currently just used for the serial console. /// NOTE: This ID must not change - to keep (minimal) compatibility with <1.2 version of android apps. case rebooted(Bool) + /// + /// Include module config + case moduleConfig(ModuleConfig) #if !swift(>=4.1) static func ==(lhs: FromRadio.OneOf_PayloadVariant, rhs: FromRadio.OneOf_PayloadVariant) -> Bool { @@ -1924,6 +1937,10 @@ struct FromRadio { guard case .rebooted(let l) = lhs, case .rebooted(let r) = rhs else { preconditionFailure() } return l == r }() + case (.moduleConfig, .moduleConfig): return { + guard case .moduleConfig(let l) = lhs, case .moduleConfig(let r) = rhs else { preconditionFailure() } + return l == r + }() default: return false } } @@ -3297,6 +3314,7 @@ extension FromRadio: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementation 7: .standard(proto: "log_record"), 8: .standard(proto: "config_complete_id"), 9: .same(proto: "rebooted"), + 10: .same(proto: "moduleConfig"), ] fileprivate class _StorageClass { @@ -3397,6 +3415,19 @@ extension FromRadio: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementation _storage._payloadVariant = .rebooted(v) } }() + case 10: try { + var v: ModuleConfig? + var hadOneofValue = false + if let current = _storage._payloadVariant { + hadOneofValue = true + if case .moduleConfig(let m) = current {v = m} + } + try decoder.decodeSingularMessageField(value: &v) + if let v = v { + if hadOneofValue {try decoder.handleConflictingOneOf()} + _storage._payloadVariant = .moduleConfig(v) + } + }() case 11: try { var v: MeshPacket? var hadOneofValue = false @@ -3450,6 +3481,10 @@ extension FromRadio: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementation guard case .rebooted(let v)? = _storage._payloadVariant else { preconditionFailure() } try visitor.visitSingularBoolField(value: v, fieldNumber: 9) }() + case .moduleConfig?: try { + guard case .moduleConfig(let v)? = _storage._payloadVariant else { preconditionFailure() } + try visitor.visitSingularMessageField(value: v, fieldNumber: 10) + }() case .packet?: try { guard case .packet(let v)? = _storage._payloadVariant else { preconditionFailure() } try visitor.visitSingularMessageField(value: v, fieldNumber: 11) diff --git a/Meshtastic/Views/ContentView.swift b/Meshtastic/Views/ContentView.swift index fec35163..e7d331ce 100644 --- a/Meshtastic/Views/ContentView.swift +++ b/Meshtastic/Views/ContentView.swift @@ -27,13 +27,6 @@ struct ContentView: View { } .tag(Tab.contacts) -// Channels() -// .tabItem { -// Label("Messages", systemImage: "text.bubble") -// .symbolRenderingMode(.hierarchical) -// .symbolVariant(.none) -// } -// .tag(Tab.messages) Connect() .tabItem { Label("Bluetooth", systemImage: "antenna.radiowaves.left.and.right") diff --git a/Meshtastic/Views/Settings/AppSettings.swift b/Meshtastic/Views/Settings/AppSettings.swift index 6e6aa954..ba546345 100644 --- a/Meshtastic/Views/Settings/AppSettings.swift +++ b/Meshtastic/Views/Settings/AppSettings.swift @@ -157,16 +157,9 @@ struct AppSettings: View { } Section(header: Text("DEBUG")) { - Toggle(isOn: $userSettings.meshActivityLog) { - Label("Log all Mesh activity", systemImage: "network") - } - .toggleStyle(SwitchToggleStyle(tint: .accentColor)) - if userSettings.meshActivityLog { - NavigationLink(destination: MeshLog()) { - Text("View Mesh Log") - } - .listRowSeparator(.visible) + NavigationLink(destination: MeshLog()) { + Text("View Mesh Log") } } } diff --git a/Meshtastic/Views/Settings/Config/DisplayConfig.swift b/Meshtastic/Views/Settings/Config/DisplayConfig.swift index 9cc4e1b6..dfa0436a 100644 --- a/Meshtastic/Views/Settings/Config/DisplayConfig.swift +++ b/Meshtastic/Views/Settings/Config/DisplayConfig.swift @@ -56,7 +56,7 @@ enum GpsFormats: Int, CaseIterable, Identifiable { } // Default of 0 is One Minute -enum ScreenOnSeconds: Int, CaseIterable, Identifiable { +enum ScreenOnIntervals: Int, CaseIterable, Identifiable { case fifteenSeconds = 15 case thirtySeconds = 30 @@ -90,7 +90,7 @@ enum ScreenOnSeconds: Int, CaseIterable, Identifiable { } // Default of 0 is off -enum ScreenCarouselSeconds: Int, CaseIterable, Identifiable { +enum ScreenCarouselIntervals: Int, CaseIterable, Identifiable { case off = 0 case fifteenSeconds = 15 @@ -145,8 +145,8 @@ struct DisplayConfig: View { Section(header: Text("Timing")) { Picker("Screen on for", selection: $screenOnSeconds ) { - ForEach(ScreenOnSeconds.allCases) { sos in - Text(sos.description) + ForEach(ScreenOnIntervals.allCases) { soi in + Text(soi.description) } } .pickerStyle(DefaultPickerStyle()) @@ -155,8 +155,8 @@ struct DisplayConfig: View { .font(.caption) Picker("Carousel Interval", selection: $screenCarouselInterval ) { - ForEach(ScreenCarouselSeconds.allCases) { scs in - Text(scs.description) + ForEach(ScreenCarouselIntervals.allCases) { sci in + Text(sci.description) } } .pickerStyle(DefaultPickerStyle()) diff --git a/Meshtastic/Views/Settings/Config/Module/ExternalNotificationConfig.swift b/Meshtastic/Views/Settings/Config/Module/ExternalNotificationConfig.swift index 538f46ef..200e5e1e 100644 --- a/Meshtastic/Views/Settings/Config/Module/ExternalNotificationConfig.swift +++ b/Meshtastic/Views/Settings/Config/Module/ExternalNotificationConfig.swift @@ -6,6 +6,46 @@ // import SwiftUI +enum OutputIntervals: Int, CaseIterable, Identifiable { + + case oneSecond = 0 + case twoSeconds = 2000 + case threeSeconds = 3000 + case fourSeconds = 4000 + case fiveSeconds = 5000 + case tenSeconds = 10000 + case fifteenSeconds = 15000 + case thirtySeconds = 30000 + case oneMinute = 60000 + + var id: Int { self.rawValue } + var description: String { + get { + switch self { + + case .oneSecond: + return "One Second" + case .twoSeconds: + return "Two Seconds" + case .threeSeconds: + return "Three Seconds" + case .fourSeconds: + return "Four Seconds" + case .fiveSeconds: + return "Five Seconds" + case .tenSeconds: + return "Ten Seconds" + case .fifteenSeconds: + return "Fifteen Seconds" + case .thirtySeconds: + return "Thirty Seconds" + case .oneMinute: + return "One Minute" + } + } + } +} + struct ExternalNotificationConfig: View { @Environment(\.managedObjectContext) var context @@ -75,6 +115,16 @@ struct ExternalNotificationConfig: View { Text("Specifies the GPIO that your external circuit is attached to on the device.") .font(.caption) .listRowSeparator(.visible) + + Picker("GPIO Output Duration", selection: $outputMilliseconds ) { + ForEach(OutputIntervals.allCases) { oi in + Text(oi.description) + } + } + .pickerStyle(DefaultPickerStyle()) + Text("Specifies how long the monitored GPIO should output.") + .font(.caption) + .listRowSeparator(.visible) } } .navigationTitle("External Notification Config") diff --git a/Meshtastic/Views/Settings/Config/Module/RangeTestConfig.swift b/Meshtastic/Views/Settings/Config/Module/RangeTestConfig.swift index efbf84cc..274707ae 100644 --- a/Meshtastic/Views/Settings/Config/Module/RangeTestConfig.swift +++ b/Meshtastic/Views/Settings/Config/Module/RangeTestConfig.swift @@ -6,6 +6,37 @@ // import SwiftUI +// Default of 0 is off +enum SenderIntervals: Int, CaseIterable, Identifiable { + + case off = 0 + case thirtySeconds = 30 + case oneMinute = 60 + case fiveMinutes = 300 + case tenMinutes = 600 + case fifteenMinutes = 900 + + var id: Int { self.rawValue } + var description: String { + get { + switch self { + case .off: + return "Off" + case .thirtySeconds: + return "Thirty Seconds" + case .oneMinute: + return "One Minute" + case .fiveMinutes: + return "Five Minutes" + case .tenMinutes: + return "Ten Minutes" + case .fifteenMinutes: + return "Fifteen Minutes" + } + } + } +} + struct RangeTestConfig: View { @Environment(\.managedObjectContext) var context @@ -18,7 +49,7 @@ struct RangeTestConfig: View { @State var hasChanges = false @State var enabled = false - @State var sender = false + @State var sender = 0 @State var save = false var body: some View { @@ -35,12 +66,13 @@ struct RangeTestConfig: View { } .toggleStyle(SwitchToggleStyle(tint: .accentColor)) - Toggle(isOn: $sender) { - - Label("Sender", systemImage: "paperplane") + Picker("Sender Interval", selection: $sender ) { + ForEach(SenderIntervals.allCases) { sci in + Text(sci.description) + } } - .toggleStyle(SwitchToggleStyle(tint: .accentColor)) - Text("This device will send out range test messages.") + .pickerStyle(DefaultPickerStyle()) + Text("This device will send out range test messages on the selected interval.") .font(.caption) Toggle(isOn: $save) { @@ -52,8 +84,8 @@ struct RangeTestConfig: View { Text("Saves a CSV with the range test message details, only available on ESP32 devices with a web server.") .font(.caption) } - } + .disabled(!(node.myInfo?.hasWifi ?? false)) Button { @@ -63,7 +95,7 @@ struct RangeTestConfig: View { Label("Save", systemImage: "square.and.arrow.down") } - .disabled(bleManager.connectedPeripheral == nil || !hasChanges) + .disabled(bleManager.connectedPeripheral == nil || !hasChanges || !(node.myInfo?.hasWifi ?? false)) .buttonStyle(.bordered) .buttonBorderShape(.capsule) .controlSize(.large) @@ -78,7 +110,7 @@ struct RangeTestConfig: View { var rtc = ModuleConfig.RangeTestConfig() rtc.enabled = enabled rtc.save = save - rtc.sender = sender ? 1 : 0 + rtc.sender = UInt32(sender) if bleManager.saveRangeTestModuleConfig(config: rtc, destNum: bleManager.connectedPeripheral.num, wantResponse: false) { @@ -104,34 +136,26 @@ struct RangeTestConfig: View { if self.initialLoad{ self.bleManager.context = context -// self.enabled = node.rangeTestConfig?.enabled ?? false -// self.save = node.rangeTestConfig?.save ?? false -// -// if node.rangeTestConfig?.sender != nil { -// -// self.sender = node.rangeTestConfig!.sender == 1 ? true : false -// -// } else { -// self.sender = false -// } -// self.sender = node.rangeTestConfig?.sender != nil + self.enabled = node.rangeTestConfig?.enabled ?? false + self.save = node.rangeTestConfig?.save ?? false + self.sender = Int(node.rangeTestConfig?.sender ?? 0) self.hasChanges = false self.initialLoad = false } } .onChange(of: enabled) { newEnabled in - //if newEnabled != node.rangeTestConfig!.enabled { + if newEnabled != node.rangeTestConfig!.enabled { hasChanges = true - //} + } } .onChange(of: save) { newSave in - //if newSave != node.rangeTestConfig!.save { + if newSave != node.rangeTestConfig!.save { hasChanges = true - //} + } } .onChange(of: sender) { newSender in diff --git a/Meshtastic/Views/Settings/Settings.swift b/Meshtastic/Views/Settings/Settings.swift index 8f6947fa..9c1bebc3 100644 --- a/Meshtastic/Views/Settings/Settings.swift +++ b/Meshtastic/Views/Settings/Settings.swift @@ -43,12 +43,13 @@ struct Settings: View { Section("Radio Configuration") { NavigationLink { - ShareChannel() + ShareChannel(node: nodes.first(where: { $0.num == connectedNodeNum }) ?? NodeInfoEntity()) } label: { Image(systemName: "qrcode") .symbolRenderingMode(.hierarchical) Text("Share Channel QR Code") } + .disabled(bleManager.connectedPeripheral == nil) Text("Radio config views will be be enabled when there is a connected node. Save buttons will be enabled when there are config changes to save.") .font(.caption) @@ -144,8 +145,9 @@ struct Settings: View { Text("Range Test (ESP32 Only)") } - .disabled(!(bleManager.connectedPeripheral == nil)) - //nodes.first(where: { $0.num == connectedNodeNum })?.myInfo?.hasWifi ?? true) || + .disabled(true) + //.disabled(bleManager.connectedPeripheral == nil) + //nodes.first(where: { $0.num == connectedNodeNum })?.myInfo?.hasWifi ?? true)//|| // nodes.first(where: { $0.num == connectedNodeNum })!.rangeTestConfig != nil) NavigationLink { diff --git a/Meshtastic/Views/Settings/ShareChannel.swift b/Meshtastic/Views/Settings/ShareChannel.swift index 5ae5dfa1..4e428f15 100644 --- a/Meshtastic/Views/Settings/ShareChannel.swift +++ b/Meshtastic/Views/Settings/ShareChannel.swift @@ -37,6 +37,8 @@ struct ShareChannel: View { @EnvironmentObject var bleManager: BLEManager @EnvironmentObject var userSettings: UserSettings + var node: NodeInfoEntity + @State private var text = "https://meshtastic.org/e/#test" var qrCodeImage = QrCodeImage()