From bc39cbd2b77e9e9da024af4c6e38c35254af2080 Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Thu, 15 Jan 2026 09:38:49 -0800 Subject: [PATCH 1/4] Make BLE Transport an actor to fix background discovery crashes (#1554) * Bump version * update the translations (#1540) update the translations * Don't alert (with sound: .default) when updating Live Activity (#1536) * Fix adding channels (#1532) * Full translation into Spanish (#1529) * tapback with any emoji (#1538) * Call clearStaleNodes at start of sendWantConfig (#1535) * NFC Tag contact (#1537) * Accessorymanager background discovery (#1542) * Don't add new BLE devices to the device list in the backgournd * Bump version * Update Meshtastic/MeshtasticApp.swift Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Update Meshtastic/MeshtasticApp.swift Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --------- Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Revert "Full translation into Spanish (#1529)" (#1543) This reverts commit f25fdfb89fba70d22cf1d281c62f956f94d6343c. * Revert "update the translations (#1540)" (#1544) This reverts commit cb2fd8cc15185f6b9ce8a940d8ca8d11a32a2f80. * Revert "NFC Tag contact (#1537)" (#1545) This reverts commit 5c22b8b6e0176f4927bfc79234dabe109b215edf. * Update Meshtastic/Views/Messages/TapbackInputView.swift Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Update Meshtastic/Helpers/EmojiOnlyTextField.swift Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Revert "Accessorymanager background discovery (#1542)" (#1553) This reverts commit 487f24b99a4f3d0b4491ee7a2c86dcffb7f62c7f. * Make BLE Transport an actor to fix background discovery crashes * Update protobufs * Protobufs * Remove UI Kit code, clean up waypoint form emoji picker * Remove redundant nested Task in tapback emoji handler (#1552) * Initial plan * Remove nested Task block in tapback handler Co-authored-by: garthvh <1795163+garthvh@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> * Delete empty file * Handle nil for emoji keyboard type extension * Remove UI kit method from waypoint form emoji picker * Remove UI kit emoji picker from tapback * Update Meshtastic/Accessory/Transports/Bluetooth Low Energy/BLETransport.swift Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Throw too many retries error again, remove return --------- Co-authored-by: MGJ <62177301+MGJ520@users.noreply.github.com> Co-authored-by: Mike Robbins Co-authored-by: Benjamin Faershtein <119711889+RCGV1@users.noreply.github.com> Co-authored-by: Alvaro Samudio Co-authored-by: Mathew Kamkar <578302+matkam@users.noreply.github.com> Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> Co-authored-by: Ben Meadors Co-authored-by: Copilot <198982749+Copilot@users.noreply.github.com> Co-authored-by: garthvh <1795163+garthvh@users.noreply.github.com> --- Localizable.xcstrings | 12 +- Meshtastic.xcodeproj/project.pbxproj | 20 ++- .../AccessoryManager+Connect.swift | 1 - .../AccessoryManager+Discovery.swift | 2 +- .../AccessoryManager+ToRadio.swift | 78 +++++---- .../Accessory Manager/AccessoryManager.swift | 2 + .../Accessory/Protocols/Connection.swift | 2 +- .../Accessory/Protocols/Transport.swift | 4 +- .../Bluetooth Low Energy/BLEConnection.swift | 10 +- .../Bluetooth Low Energy/BLETransport.swift | 86 ++++++---- Meshtastic/Extensions/UIKeyboardType.swift | 13 ++ Meshtastic/Extensions/UserDefaults.swift | 4 + Meshtastic/Helpers/EmojiOnlyTextField.swift | 46 ++++++ Meshtastic/Helpers/MeshPackets.swift | 4 +- .../Messages/MessageContextMenuItems.swift | 27 +--- Meshtastic/Views/Messages/MessageText.swift | 36 ++++- .../Views/Messages/TapbackInputView.swift | 80 ++++++++++ .../Nodes/Helpers/Map/WaypointForm.swift | 13 +- Meshtastic/Views/Nodes/MeshMap.swift | 6 +- Meshtastic/Views/Settings/AppSettings.swift | 2 +- .../Sources/meshtastic/admin.pb.swift | 149 +++++++++++++++++- .../Sources/meshtastic/mesh.pb.swift | 40 ++++- protobufs | 2 +- 23 files changed, 499 insertions(+), 140 deletions(-) create mode 100644 Meshtastic/Extensions/UIKeyboardType.swift create mode 100644 Meshtastic/Views/Messages/TapbackInputView.swift diff --git a/Localizable.xcstrings b/Localizable.xcstrings index 33be032d..bc642a1a 100644 --- a/Localizable.xcstrings +++ b/Localizable.xcstrings @@ -14315,6 +14315,7 @@ } }, "Favorited and ignored nodes are always retained. Nodes without PKC keys are cleared from the app database on the schedule set by the user, nodes with PKC keys are cleared only if the interval is set to 7 days or longer. This feature only purges nodes from the app that are not stored in the device node database." : { + "extractionState" : "stale", "localizations" : { "de" : { "stringUnit" : { @@ -14335,6 +14336,9 @@ } } } + }, + "Favorited and ignored nodes are always retained. Other nodes are cleared from the app database on the schedule set by the user. (Nodes with PKC keys are always retained for at least 7 days.) This feature only purges nodes from the app that are not stored in the device node database." : { + }, "Favorites" : { "localizations" : { @@ -31617,6 +31621,9 @@ } } } + }, + "Select an emoji" : { + }, "Select Channel" : { "localizations" : { @@ -35547,6 +35554,9 @@ } } } + }, + "Tap to enter emoji" : { + }, "Tapback" : { "localizations" : { @@ -42321,4 +42331,4 @@ } }, "version" : "1.1" -} +} \ No newline at end of file diff --git a/Meshtastic.xcodeproj/project.pbxproj b/Meshtastic.xcodeproj/project.pbxproj index b87ce5f2..cc68584c 100644 --- a/Meshtastic.xcodeproj/project.pbxproj +++ b/Meshtastic.xcodeproj/project.pbxproj @@ -115,6 +115,7 @@ D93068D52B812B700066FBC8 /* MessageDestination.swift in Sources */ = {isa = PBXBuildFile; fileRef = D93068D42B812B700066FBC8 /* MessageDestination.swift */; }; D93068D72B8146690066FBC8 /* MessageText.swift in Sources */ = {isa = PBXBuildFile; fileRef = D93068D62B8146690066FBC8 /* MessageText.swift */; }; D93068D92B81509C0066FBC8 /* TapbackResponses.swift in Sources */ = {isa = PBXBuildFile; fileRef = D93068D82B81509C0066FBC8 /* TapbackResponses.swift */; }; + D93068DA2B81509D0066FBC8 /* TapbackInputView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D93068D92B81509D0066FBC8 /* TapbackInputView.swift */; }; D93068DB2B81C85E0066FBC8 /* PowerConfig.swift in Sources */ = {isa = PBXBuildFile; fileRef = D93068DA2B81C85E0066FBC8 /* PowerConfig.swift */; }; D93068DD2B81CA820066FBC8 /* ConfigHeader.swift in Sources */ = {isa = PBXBuildFile; fileRef = D93068DC2B81CA820066FBC8 /* ConfigHeader.swift */; }; D93069082B81DF040066FBC8 /* SaveConfigButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = D93069072B81DF040066FBC8 /* SaveConfigButton.swift */; }; @@ -201,7 +202,6 @@ DD93800B2BA3F968008BEC06 /* NodeMapContent.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD93800A2BA3F968008BEC06 /* NodeMapContent.swift */; }; DD93800E2BA74D0C008BEC06 /* ChannelForm.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD93800D2BA74D0C008BEC06 /* ChannelForm.swift */; }; DD94B7402ACCE3BE00DCD1D1 /* MapSettingsForm.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD94B73F2ACCE3BE00DCD1D1 /* MapSettingsForm.swift */; }; - DD964FBD296E6B01007C176F /* EmojiOnlyTextField.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD964FBC296E6B01007C176F /* EmojiOnlyTextField.swift */; }; DD964FC2297272AE007C176F /* WaypointEntityExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD964FC1297272AE007C176F /* WaypointEntityExtension.swift */; }; DD964FC62975DBFD007C176F /* QueryCoreData.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD964FC52975DBFD007C176F /* QueryCoreData.swift */; }; DD97E96628EFD9820056DDA4 /* MeshtasticLogo.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD97E96528EFD9820056DDA4 /* MeshtasticLogo.swift */; }; @@ -215,6 +215,7 @@ DD9C70112E916EBD00106227 /* UpdateIntervalPicker.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD9C70102E916EA200106227 /* UpdateIntervalPicker.swift */; }; DDA0B6B2294CDC55001356EC /* Channels.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDA0B6B1294CDC55001356EC /* Channels.swift */; }; DDA1C48E28DB49D3009933EC /* ChannelRoles.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDA1C48D28DB49D3009933EC /* ChannelRoles.swift */; }; + DDA3DFDA2F10B39600D8F103 /* UIKeyboardType.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDA3DFD92F10B39600D8F103 /* UIKeyboardType.swift */; }; DDA6B2E928419CF2003E8C16 /* MeshPackets.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDA6B2E828419CF2003E8C16 /* MeshPackets.swift */; }; DDA9515A2BC6624100CEA535 /* TelemetryWeather.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDA951592BC6624100CEA535 /* TelemetryWeather.swift */; }; DDA9515C2BC6631200CEA535 /* TelemetryEnums.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDA9515B2BC6631200CEA535 /* TelemetryEnums.swift */; }; @@ -427,6 +428,7 @@ D93068D42B812B700066FBC8 /* MessageDestination.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageDestination.swift; sourceTree = ""; }; D93068D62B8146690066FBC8 /* MessageText.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageText.swift; sourceTree = ""; }; D93068D82B81509C0066FBC8 /* TapbackResponses.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TapbackResponses.swift; sourceTree = ""; }; + D93068D92B81509D0066FBC8 /* TapbackInputView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TapbackInputView.swift; sourceTree = ""; }; D93068DA2B81C85E0066FBC8 /* PowerConfig.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PowerConfig.swift; sourceTree = ""; }; D93068DC2B81CA820066FBC8 /* ConfigHeader.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConfigHeader.swift; sourceTree = ""; }; D93069062B81D8900066FBC8 /* MeshtasticDataModelV 27.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = "MeshtasticDataModelV 27.xcdatamodel"; sourceTree = ""; }; @@ -545,7 +547,6 @@ DD93800A2BA3F968008BEC06 /* NodeMapContent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NodeMapContent.swift; sourceTree = ""; }; DD93800D2BA74D0C008BEC06 /* ChannelForm.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChannelForm.swift; sourceTree = ""; }; DD94B73F2ACCE3BE00DCD1D1 /* MapSettingsForm.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MapSettingsForm.swift; sourceTree = ""; }; - DD964FBC296E6B01007C176F /* EmojiOnlyTextField.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EmojiOnlyTextField.swift; sourceTree = ""; }; DD964FC029724F6D007C176F /* MeshtasticDataModelV6.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = MeshtasticDataModelV6.xcdatamodel; sourceTree = ""; }; DD964FC1297272AE007C176F /* WaypointEntityExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WaypointEntityExtension.swift; sourceTree = ""; }; DD964FC52975DBFD007C176F /* QueryCoreData.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = QueryCoreData.swift; sourceTree = ""; }; @@ -563,6 +564,7 @@ DDA0B6B1294CDC55001356EC /* Channels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Channels.swift; sourceTree = ""; }; DDA1C48D28DB49D3009933EC /* ChannelRoles.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChannelRoles.swift; sourceTree = ""; }; DDA28B1B2D32C89200EF726F /* MeshtasticDataModelV 48.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = "MeshtasticDataModelV 48.xcdatamodel"; sourceTree = ""; }; + DDA3DFD92F10B39600D8F103 /* UIKeyboardType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIKeyboardType.swift; sourceTree = ""; }; DDA6B2E828419CF2003E8C16 /* MeshPackets.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MeshPackets.swift; sourceTree = ""; }; DDA951592BC6624100CEA535 /* TelemetryWeather.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TelemetryWeather.swift; sourceTree = ""; }; DDA9515B2BC6631200CEA535 /* TelemetryEnums.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TelemetryEnums.swift; sourceTree = ""; }; @@ -1250,6 +1252,7 @@ D93068D62B8146690066FBC8 /* MessageText.swift */, D93068D22B8129510066FBC8 /* MessageContextMenuItems.swift */, D93068D82B81509C0066FBC8 /* TapbackResponses.swift */, + D93068D92B81509D0066FBC8 /* TapbackInputView.swift */, ); path = Messages; sourceTree = ""; @@ -1291,7 +1294,6 @@ DD1BEF492E0292220090CE24 /* KeychainHelper.swift */, DD913638270DFF4C00D7ACF3 /* LocalNotificationManager.swift */, DDA6B2E828419CF2003E8C16 /* MeshPackets.swift */, - DD964FBC296E6B01007C176F /* EmojiOnlyTextField.swift */, DD3619142B1EF9F900C41C8C /* LocationsHandler.swift */, 6D825E612C34786C008DBEE4 /* CommonRegex.swift */, 3D3417B32E2730EC006A988B /* GeoJSONOverlayManager.swift */, @@ -1371,6 +1373,7 @@ DDDB444729F8A9C900EE2349 /* String.swift */, DD77093E2AA1B146007A8BF0 /* UIColor.swift */, DDDB444F29F8AC9C00EE2349 /* UIImage.swift */, + DDA3DFD92F10B39600D8F103 /* UIKeyboardType.swift */, DDDB443F29F79AB000EE2349 /* UserDefaults.swift */, DDB75A0E2A05920E006ED576 /* FileManager.swift */, DDB75A102A059258006ED576 /* Url.swift */, @@ -1668,7 +1671,6 @@ DD77093B2AA1ABB8007A8BF0 /* BluetoothTips.swift in Sources */, D9C9839D2B79CFD700BDBE6A /* TextMessageSize.swift in Sources */, DDC94FCE29CF55310082EA6E /* RtttlConfig.swift in Sources */, - DD964FBD296E6B01007C176F /* EmojiOnlyTextField.swift in Sources */, DD1BEF4E2E03916A0090CE24 /* ChannelsHelp.swift in Sources */, DD8169FF272476C700F4AB02 /* LogDocument.swift in Sources */, BCD93CBA2D9E11A2006C9214 /* DisconnectNodeIntent.swift in Sources */, @@ -1801,6 +1803,7 @@ DDA6B2E928419CF2003E8C16 /* MeshPackets.swift in Sources */, DDCE4E2C2869F92900BE9F8F /* UserConfig.swift in Sources */, BCB613852C68703800485544 /* NodePositionIntent.swift in Sources */, + DDA3DFDA2F10B39600D8F103 /* UIKeyboardType.swift in Sources */, DDB75A212A12B954006ED576 /* LoRaSignalStrength.swift in Sources */, DD6193752862F6E600E59241 /* ExternalNotificationConfig.swift in Sources */, DD268D8E2BCC90E2008073AE /* RouteEnums.swift in Sources */, @@ -1809,6 +1812,7 @@ DD4975A52B147BA90026544E /* AmbientLightingConfig.swift in Sources */, 3D3417D22E2DC260006A988B /* MapDataManager.swift in Sources */, D93068D92B81509C0066FBC8 /* TapbackResponses.swift in Sources */, + D93068DA2B81509D0066FBC8 /* TapbackInputView.swift in Sources */, DDA9F5E82E77FAC100E70DEB /* AnimatedNodePin.swift in Sources */, DDF82CBD2D5BC69200DC25EC /* NavigateToButton.swift in Sources */, 8D3F8A3F2D44BB02009EAAA4 /* PowerMetrics.swift in Sources */, @@ -2102,7 +2106,7 @@ "@executable_path/Frameworks", ); MACOSX_DEPLOYMENT_TARGET = 14.6; - MARKETING_VERSION = 2.7.6; + MARKETING_VERSION = 2.7.7; PRODUCT_BUNDLE_IDENTIFIER = gvh.MeshtasticClient; PRODUCT_NAME = "$(TARGET_NAME)"; SUPPORTS_MACCATALYST = YES; @@ -2137,7 +2141,7 @@ "@executable_path/Frameworks", ); MACOSX_DEPLOYMENT_TARGET = 14.6; - MARKETING_VERSION = 2.7.6; + MARKETING_VERSION = 2.7.7; PRODUCT_BUNDLE_IDENTIFIER = gvh.MeshtasticClient; PRODUCT_NAME = "$(TARGET_NAME)"; SUPPORTS_MACCATALYST = YES; @@ -2169,7 +2173,7 @@ "@executable_path/../../Frameworks", ); MACOSX_DEPLOYMENT_TARGET = 14.6; - MARKETING_VERSION = 2.7.6; + MARKETING_VERSION = 2.7.7; PRODUCT_BUNDLE_IDENTIFIER = gvh.MeshtasticClient.Widgets; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; @@ -2202,7 +2206,7 @@ "@executable_path/../../Frameworks", ); MACOSX_DEPLOYMENT_TARGET = 14.6; - MARKETING_VERSION = 2.7.6; + MARKETING_VERSION = 2.7.7; PRODUCT_BUNDLE_IDENTIFIER = gvh.MeshtasticClient.Widgets; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; diff --git a/Meshtastic/Accessory/Accessory Manager/AccessoryManager+Connect.swift b/Meshtastic/Accessory/Accessory Manager/AccessoryManager+Connect.swift index d5ca0929..1ec87149 100644 --- a/Meshtastic/Accessory/Accessory Manager/AccessoryManager+Connect.swift +++ b/Meshtastic/Accessory/Accessory Manager/AccessoryManager+Connect.swift @@ -352,7 +352,6 @@ actor SequentialSteps { return } isRunning = false - return throw AccessoryError.tooManyRetries } diff --git a/Meshtastic/Accessory/Accessory Manager/AccessoryManager+Discovery.swift b/Meshtastic/Accessory/Accessory Manager/AccessoryManager+Discovery.swift index 831ffe30..1a0e9ebd 100644 --- a/Meshtastic/Accessory/Accessory Manager/AccessoryManager+Discovery.swift +++ b/Meshtastic/Accessory/Accessory Manager/AccessoryManager+Discovery.swift @@ -15,7 +15,7 @@ extension AccessoryManager { let tasks = transports.map { transport in Task { Logger.transport.info("🔎 [Discovery] Discovery stream started for transport \(String(describing: transport.type), privacy: .public)") - for await event in transport.discoverDevices() { + for await event in await transport.discoverDevices() { continuation.yield(event) } Logger.transport.info("🔎 [Discovery] Discovery stream closed for transport \(String(describing: transport.type), privacy: .public)") diff --git a/Meshtastic/Accessory/Accessory Manager/AccessoryManager+ToRadio.swift b/Meshtastic/Accessory/Accessory Manager/AccessoryManager+ToRadio.swift index 4fe2ffaf..cd1d2961 100644 --- a/Meshtastic/Accessory/Accessory Manager/AccessoryManager+ToRadio.swift +++ b/Meshtastic/Accessory/Accessory Manager/AccessoryManager+ToRadio.swift @@ -441,8 +441,6 @@ extension AccessoryManager { Logger.services.error("Error while sending saveChannelSet request. No active device.") throw AccessoryError.ioFailed("No active device") } - var i: Int32 = 0 - var myInfo: MyInfoEntity // Before we get started delete the existing channels from the myNodeInfo if !addChannels { tryClearExistingChannels() @@ -451,64 +449,74 @@ extension AccessoryManager { let decodedString = base64UrlString.base64urlToBase64() if let decodedData = Data(base64Encoded: decodedString) { let channelSet: ChannelSet = try ChannelSet(serializedBytes: decodedData) + + var myInfo: MyInfoEntity! + var i: Int32 = 0 + + if addChannels { + let fetchMyInfoRequest = MyInfoEntity.fetchRequest() + fetchMyInfoRequest.predicate = NSPredicate(format: "myNodeNum == %lld", Int64(deviceNum)) + + let fetchedMyInfo = try context.fetch(fetchMyInfoRequest) + if fetchedMyInfo.count != 1 { + throw AccessoryError.appError("MyInfo not found") + } + + // We are trying to add a channel so lets get the last index + myInfo = fetchedMyInfo[0] + i = Int32(myInfo.channels?.count ?? -1) + + // Bail out if the index is negative or bigger than our max of 8 + if i < 0 || i > 8 { + throw AccessoryError.appError("Index out of range \(i)") + } + } + for cs in channelSet.settings { + if addChannels { - // We are trying to add a channel so lets get the last index - let fetchMyInfoRequest = MyInfoEntity.fetchRequest() - fetchMyInfoRequest.predicate = NSPredicate(format: "myNodeNum == %lld", Int64(deviceNum)) - do { - let fetchedMyInfo = try context.fetch(fetchMyInfoRequest) - if fetchedMyInfo.count == 1 { - i = Int32(fetchedMyInfo[0].channels?.count ?? -1) - myInfo = fetchedMyInfo[0] - // Bail out if the index is negative or bigger than our max of 8 - if i < 0 || i > 8 { - throw AccessoryError.appError("Index out of range \(i)") - } - // Bail out if there are no channels or if the same channel name already exists - guard let mutableChannels = myInfo.channels!.mutableCopy() as? NSMutableOrderedSet else { - throw AccessoryError.appError("No channels or channel") - } - if mutableChannels.first(where: {($0 as AnyObject).name == cs.name }) is ChannelEntity { - throw AccessoryError.appError("Channel already exists") - } - } - } catch { - Logger.data.error("Failed to find a node MyInfo to save these channels to: \(error.localizedDescription, privacy: .public)") + guard let mutableChannels = myInfo.channels?.mutableCopy() as? NSMutableOrderedSet else { + throw AccessoryError.appError("No channels or channel") + } + + // Bail out if there are no channels or if the same channel name already exists + if mutableChannels.first(where: { ($0 as AnyObject).name == cs.name }) is ChannelEntity { + throw AccessoryError.appError("Channel already exists") } } var chan = Channel() - if i == 0 { - chan.role = Channel.Role.primary - } else { - chan.role = Channel.Role.secondary - } + chan.role = (i == 0) ? .primary : .secondary chan.settings = cs chan.index = i i += 1 var adminPacket = AdminMessage() adminPacket.setChannel = chan - var meshPacket: MeshPacket = MeshPacket() + + var meshPacket = MeshPacket() meshPacket.to = UInt32(deviceNum) - meshPacket.from = UInt32(deviceNum) + meshPacket.from = UInt32(deviceNum) meshPacket.id = UInt32.random(in: UInt32(UInt8.max).. AsyncStream - func disconnect(withError: Error?, shouldReconnect: Bool) throws + func disconnect(withError: Error?, shouldReconnect: Bool) async throws func drainPendingPackets() async throws func startDrainPendingPackets() throws diff --git a/Meshtastic/Accessory/Protocols/Transport.swift b/Meshtastic/Accessory/Protocols/Transport.swift index 55fa8545..af291869 100644 --- a/Meshtastic/Accessory/Protocols/Transport.swift +++ b/Meshtastic/Accessory/Protocols/Transport.swift @@ -42,10 +42,10 @@ enum DiscoveryEvent { protocol Transport { var type: TransportType { get } - var status: TransportStatus { get } + var status: TransportStatus { get async } // Discovers devices asynchronously. For ongoing scans (e.g., BLE), this can yield via AsyncStream. - func discoverDevices() -> AsyncStream + func discoverDevices() async -> AsyncStream // Connects to a device and returns a Connection. func connect(to device: Device) async throws -> any Connection diff --git a/Meshtastic/Accessory/Transports/Bluetooth Low Energy/BLEConnection.swift b/Meshtastic/Accessory/Transports/Bluetooth Low Energy/BLEConnection.swift index f7a0f012..a1513061 100644 --- a/Meshtastic/Accessory/Transports/Bluetooth Low Energy/BLEConnection.swift +++ b/Meshtastic/Accessory/Transports/Bluetooth Low Energy/BLEConnection.swift @@ -69,7 +69,7 @@ actor BLEConnection: Connection { self.delegate.setConnection(self) } - func disconnect(withError error: Error? = nil, shouldReconnect: Bool) throws { + func disconnect(withError error: Error? = nil, shouldReconnect: Bool) async throws { if peripheral.state == .connected { if let characteristic = FROMRADIO_characteristic { peripheral.setNotifyValue(false, for: characteristic) @@ -82,7 +82,7 @@ actor BLEConnection: Connection { } } - transport?.connectionDidDisconnect(fromPeripheral: peripheral) + await transport?.connectionDidDisconnect(fromPeripheral: peripheral) central.cancelPeripheralConnection(peripheral) peripheral.delegate = nil @@ -217,8 +217,8 @@ actor BLEConnection: Connection { self.connectContinuation = nil } - private func notifyTransportOfDisconnect() { - transport?.connectionDidDisconnect(fromPeripheral: peripheral) + private func notifyTransportOfDisconnect() async { + await transport?.connectionDidDisconnect(fromPeripheral: peripheral) } func startRSSITask() { @@ -450,7 +450,7 @@ actor BLEConnection: Connection { } // Inform the active connection that there was an error and it should disconnect - try self.disconnect(withError: error, shouldReconnect: shouldReconnect) + try await self.disconnect(withError: error, shouldReconnect: shouldReconnect) } func appDidEnterBackground() { diff --git a/Meshtastic/Accessory/Transports/Bluetooth Low Energy/BLETransport.swift b/Meshtastic/Accessory/Transports/Bluetooth Low Energy/BLETransport.swift index aa1a32d4..fc4953ac 100644 --- a/Meshtastic/Accessory/Transports/Bluetooth Low Energy/BLETransport.swift +++ b/Meshtastic/Accessory/Transports/Bluetooth Low Energy/BLETransport.swift @@ -10,7 +10,7 @@ import Foundation import SwiftUI import OSLog -class BLETransport: Transport { +actor BLETransport: Transport { let meshtasticServiceCBUUID = CBUUID(string: "0x6BA1B218-15A8-461F-9FA8-5DCAE273EAFD") private let kCentralRestoreID = "com.meshtastic.central" @@ -31,7 +31,7 @@ class BLETransport: Transport { private var cleanupTask: Task? // Transport properties - var supportsManualConnection: Bool = false + let supportsManualConnection: Bool = false let requiresPeriodicHeartbeat = false init() { @@ -46,19 +46,24 @@ class BLETransport: Transport { self.delegate.setTransport(self) } - nonisolated func discoverDevices() -> AsyncStream { + private func setDiscoveredDeviceContinuation(_ cont: AsyncStream.Continuation?) { + self.discoveredDeviceContinuation = cont + } + + func discoverDevices() -> AsyncStream { AsyncStream { cont in Task { - self.discoveredDeviceContinuation = cont + await self.setDiscoveredDeviceContinuation(cont) // This gate is opened when the CBCentralManager is in poweredOn state. // Its probably open already, but just to be sure in case we get here too quickly. try await self.setupCompleteGate.wait() - if !restoreInProgress { + if await !self.restoreInProgress { centralManager.scanForPeripherals(withServices: [meshtasticServiceCBUUID], options: [CBCentralManagerScanOptionAllowDuplicatesKey: true]) - for alreadyDiscoveredPeripheral in self.discoveredPeripherals.values.map({$0.peripheral}) { + let peripherals = await self.discoveredPeripherals.values.map({$0.peripheral}) + for alreadyDiscoveredPeripheral in peripherals { let device = Device(id: alreadyDiscoveredPeripheral.identifier, name: alreadyDiscoveredPeripheral.name ?? "Unknown", transportType: .ble, @@ -66,11 +71,13 @@ class BLETransport: Transport { cont.yield(.deviceFound(device)) } } - setupCleanupTask() + await setupCleanupTask() } cont.onTermination = { _ in Logger.transport.error("🛜 [BLE] Discovery event stream has been canecelled.") - self.stopScanning() + Task { + await self.stopScanning() + } } } } @@ -188,6 +195,12 @@ class BLETransport: Transport { } } + private func cancelConnectContinuation(for peripheral: CBPeripheral) { + self.connectContinuation?.resume(throwing: CancellationError()) + self.connectContinuation = nil + self.connectionDidDisconnect(fromPeripheral: peripheral) + } + func connect(to device: Device) async throws -> any Connection { guard let peripheral = discoveredPeripherals[UUID(uuidString: device.identifier)!] else { throw AccessoryError.connectionFailed("Peripheral not found") @@ -211,9 +224,9 @@ class BLETransport: Transport { self.activeConnection = newConnection return newConnection } onCancel: { - self.connectContinuation?.resume(throwing: CancellationError()) - self.connectContinuation = nil - self.connectionDidDisconnect(fromPeripheral: peripheral.peripheral) + Task { + await self.cancelConnectContinuation(for: peripheral.peripheral) + } } Logger.transport.debug("🛜 [BLE] Connect complete.") return returnConnection @@ -226,7 +239,7 @@ class BLETransport: Transport { Task { if await connection.peripheral.identifier == peripheral.identifier { try await connection.disconnect(withError: AccessoryError.disconnected("BLE connection lost"), shouldReconnect: true) - self.connectionDidDisconnect(fromPeripheral: peripheral) + await self.connectionDidDisconnect(fromPeripheral: peripheral) } } } @@ -264,7 +277,7 @@ class BLETransport: Transport { Logger.transport.debug("🛜 [BLETransport] Error while connecting. Disconnecting the active connection.") Task { try? await activeConnection.disconnect(withError: error, shouldReconnect: shouldReconnect) - self.connectionDidDisconnect(fromPeripheral: peripheral) + await self.connectionDidDisconnect(fromPeripheral: peripheral) } } else { Logger.transport.error("🚨 [BLETransport] unhandled error. May be in an inconsistent state.") @@ -372,15 +385,20 @@ class BLETransport: Transport { } Logger.transport.error("🛜 [BLE] Restoring peripheral in connecting state. ✅ didConnect Received!") - Task { @MainActor in - // In this case we need a full reconnect, so do the wantConfig, wantDatabase, and versionCheck - try? await AccessoryManager.shared.connect(to: device, withConnection: restoredConnection, wantConfig: true, wantDatabase: true, versionCheck: true) - restoreInProgress = false + let connectTask = Task { @MainActor in + try await AccessoryManager.shared.connect(to: device, withConnection: restoredConnection, wantConfig: true, wantDatabase: true, versionCheck: true) } + + do { + try await connectTask.value + } catch { + Logger.transport.error("🛜 [BLE] Error connecting during state restoration: \(error, privacy: .public)") + } + self.restoreInProgress = false } catch { - // We had a conneciton failure during restoration. + // We had a connection failure during restoration. Logger.transport.error("🛜 [BLE] Error restoring peripheral in connecting state. \(error, privacy: .public)") - restoreInProgress = false + self.restoreInProgress = false } } @@ -388,22 +406,28 @@ class BLETransport: Transport { let restoredConnection = BLEConnection(peripheral: peripheral, central: central, transport: self) self.activeConnection = restoredConnection Logger.transport.error("🛜 [BLE] Peripheral Connection found and state is connected setting this connection as the activeConnection.") - Task { @MainActor in + let connectTask = Task { @MainActor in // In this case we need a full reconnect, so do the wantConfig, wantDatabase, and versionCheck - try? await AccessoryManager.shared.connect(to: device, withConnection: restoredConnection, wantConfig: false, wantDatabase: false, versionCheck: false) - restoreInProgress = false + try await AccessoryManager.shared.connect(to: device, withConnection: restoredConnection, wantConfig: false, wantDatabase: false, versionCheck: false) } + do { + try await connectTask.value + } catch { + Logger.transport.error("🛜 [BLE] Error connecting during state restoration: \(error, privacy: .public)") + } + + self.restoreInProgress = false Logger.transport.error("🛜 [BLE] Connection state successfully restored in the background.") default: // Since we're not going to attempt to reconnect in then allow normal device discovery Logger.transport.error("🛜 [BLE] Unhandled state restoration for state: \(cbPeripheralStateDescription(peripheral.state), privacy: .public).") - restoreInProgress = false + self.restoreInProgress = false } } } - func device(forManualConnection: String) -> Device? { + nonisolated func device(forManualConnection: String) -> Device? { return nil } @@ -438,33 +462,33 @@ class BLEDelegate: NSObject, CBCentralManagerDelegate { } func centralManagerDidUpdateState(_ central: CBCentralManager) { - transport?.handleCentralState(central.state, central: central) + Task { await transport?.handleCentralState(central.state, central: central) } } func centralManager(_ central: CBCentralManager, didDiscover peripheral: CBPeripheral, advertisementData: [String: Any], rssi RSSI: NSNumber) { - transport?.didDiscover(peripheral: peripheral, rssi: RSSI) + Task { await transport?.didDiscover(peripheral: peripheral, rssi: RSSI) } } func centralManager(_ central: CBCentralManager, didConnect peripheral: CBPeripheral) { - transport?.handleDidConnect(peripheral: peripheral, central: central) + Task { await transport?.handleDidConnect(peripheral: peripheral, central: central) } } func centralManager(_ central: CBCentralManager, didFailToConnect peripheral: CBPeripheral, error: Error?) { - transport?.handleDidFailToConnect(peripheral: peripheral, error: error) + Task { await transport?.handleDidFailToConnect(peripheral: peripheral, error: error) } } func centralManager(_ central: CBCentralManager, didDisconnectPeripheral peripheral: CBPeripheral, error: Error?) { if let error = error as? NSError { Logger.transport.error("🛜 [BLETransport] Error while disconnecting peripheral: \(peripheral.name ?? ""): \(error)") - transport?.handlePeripheralDisconnectError(peripheral: peripheral, error: error) + Task { await transport?.handlePeripheralDisconnectError(peripheral: peripheral, error: error) } } else { Logger.transport.error("🛜 [BLETransport] Did succesfully disconnect peripheral: \(peripheral.name ?? "")") - transport?.handlePeripheralDisconnect(peripheral: peripheral) + Task { await transport?.handlePeripheralDisconnect(peripheral: peripheral) } } } func centralManager(_ central: CBCentralManager, willRestoreState dict: [String: Any]) { - self.transport?.handleWillRestoreState(dict: dict, central: central) + Task { await self.transport?.handleWillRestoreState(dict: dict, central: central) } } } diff --git a/Meshtastic/Extensions/UIKeyboardType.swift b/Meshtastic/Extensions/UIKeyboardType.swift new file mode 100644 index 00000000..1353e35e --- /dev/null +++ b/Meshtastic/Extensions/UIKeyboardType.swift @@ -0,0 +1,13 @@ +// +// UIKeyboard.swift +// Meshtastic +// +// Copyright(c) Garth Vander Houwen 1/7/26. +// +import UIKit + +extension UIKeyboardType { + static var emoji: UIKeyboardType { + return UIKeyboardType(rawValue: 124) ?? .default + } +} diff --git a/Meshtastic/Extensions/UserDefaults.swift b/Meshtastic/Extensions/UserDefaults.swift index 82e67773..12bd86ee 100644 --- a/Meshtastic/Extensions/UserDefaults.swift +++ b/Meshtastic/Extensions/UserDefaults.swift @@ -80,6 +80,7 @@ extension UserDefaults { case showDeviceOnboarding case usageDataAndCrashReporting case autoconnectOnDiscovery + case purgeStaleNodeDays case manualConnections case testIntEnum } @@ -178,6 +179,9 @@ extension UserDefaults { @UserDefault(.autoconnectOnDiscovery, defaultValue: true) static var autoconnectOnDiscovery: Bool + @UserDefault(.purgeStaleNodeDays, defaultValue: 0) + static var purgeStaleNodeDays: Double + @UserDefault(.testIntEnum, defaultValue: .one) static var testIntEnum: TestIntEnum diff --git a/Meshtastic/Helpers/EmojiOnlyTextField.swift b/Meshtastic/Helpers/EmojiOnlyTextField.swift index 0982ab33..aae9e3a3 100644 --- a/Meshtastic/Helpers/EmojiOnlyTextField.swift +++ b/Meshtastic/Helpers/EmojiOnlyTextField.swift @@ -7,6 +7,7 @@ import SwiftUI class SwiftUIEmojiTextField: UITextField { + var shouldBecomeFirstResponderOnAppear = false func setEmoji() { _ = self.textInputMode @@ -23,22 +24,39 @@ class SwiftUIEmojiTextField: UITextField { } return nil } + + override func didMoveToWindow() { + super.didMoveToWindow() + if shouldBecomeFirstResponderOnAppear && window != nil { + DispatchQueue.main.async { [weak self] in + self?.becomeFirstResponder() + } + } + } } struct EmojiOnlyTextField: UIViewRepresentable { @Binding var text: String var placeholder: String = "" + var onBecomeFirstResponder: (() -> Void)? + var onKeyboardTypeChanged: ((Bool) -> Void)? // true if NOT emoji (should dismiss), false if emoji + var onKeyboardDismissed: (() -> Void)? // Called when keyboard is dismissed func makeUIView(context: Context) -> SwiftUIEmojiTextField { let emojiTextField = SwiftUIEmojiTextField() emojiTextField.placeholder = placeholder emojiTextField.text = text emojiTextField.delegate = context.coordinator + emojiTextField.shouldBecomeFirstResponderOnAppear = true + context.coordinator.textField = emojiTextField return emojiTextField } func updateUIView(_ uiView: SwiftUIEmojiTextField, context: Context) { uiView.text = text + context.coordinator.onBecomeFirstResponder = onBecomeFirstResponder + context.coordinator.onKeyboardTypeChanged = onKeyboardTypeChanged + context.coordinator.onKeyboardDismissed = onKeyboardDismissed } func makeCoordinator() -> Coordinator { @@ -47,13 +65,41 @@ struct EmojiOnlyTextField: UIViewRepresentable { class Coordinator: NSObject, UITextFieldDelegate { var parent: EmojiOnlyTextField + var textField: SwiftUIEmojiTextField? + var onBecomeFirstResponder: (() -> Void)? + var onKeyboardTypeChanged: ((Bool) -> Void)? + var onKeyboardDismissed: (() -> Void)? + var previousInputMode: String? + init(parent: EmojiOnlyTextField) { self.parent = parent } + + func textFieldDidBeginEditing(_ textField: UITextField) { + onBecomeFirstResponder?() + checkInputMode(textField) + } + + func textFieldDidEndEditing(_ textField: UITextField) { + // Keyboard was dismissed + onKeyboardDismissed?() + } + func textFieldDidChangeSelection(_ textField: UITextField) { DispatchQueue.main.async { [weak self] in self?.parent.text = textField.text ?? "" } + checkInputMode(textField) + } + + private func checkInputMode(_ textField: UITextField) { + if let inputMode = textField.textInputMode { + let isEmoji = inputMode.primaryLanguage == "emoji" + if previousInputMode != inputMode.primaryLanguage { + previousInputMode = inputMode.primaryLanguage + onKeyboardTypeChanged?(!isEmoji) // true if NOT emoji (should dismiss) + } + } } } } diff --git a/Meshtastic/Helpers/MeshPackets.swift b/Meshtastic/Helpers/MeshPackets.swift index 255417c4..8b7a7423 100644 --- a/Meshtastic/Helpers/MeshPackets.swift +++ b/Meshtastic/Helpers/MeshPackets.swift @@ -881,8 +881,8 @@ func telemetryPacket(packet: MeshPacket, connectedNode: Int64, context: NSManage let meshActivity = Activity.activities.first(where: { $0.attributes.nodeNum == connectedNode }) if meshActivity != nil { Task { - await meshActivity?.update(updatedContent, alertConfiguration: alertConfiguration) - // await meshActivity?.update(updatedContent) + // await meshActivity?.update(updatedContent, alertConfiguration: alertConfiguration) + await meshActivity?.update(updatedContent) Logger.services.debug("Updated live activity.") } } diff --git a/Meshtastic/Views/Messages/MessageContextMenuItems.swift b/Meshtastic/Views/Messages/MessageContextMenuItems.swift index 63104320..14d5b3f7 100644 --- a/Meshtastic/Views/Messages/MessageContextMenuItems.swift +++ b/Meshtastic/Views/Messages/MessageContextMenuItems.swift @@ -10,6 +10,7 @@ struct MessageContextMenuItems: View { let tapBackDestination: MessageDestination let isCurrentUser: Bool @Binding var isShowingDeleteConfirmation: Bool + @Binding var isShowingTapbackInput: Bool let onReply: () -> Void @State var relayDisplay: String? = nil @@ -29,30 +30,8 @@ struct MessageContextMenuItems: View { } } - Menu("Tapback") { - ForEach(Tapbacks.allCases) { tb in - Button { - Task { - do { - try await accessoryManager.sendMessage( - message: tb.emojiString, - toUserNum: tapBackDestination.userNum, - channel: tapBackDestination.channelNum, - isEmoji: true, - replyID: message.messageId - ) - Task { @MainActor in - self.context.refresh(tapBackDestination.managedObject, mergeChanges: true) - } - } catch { - Logger.services.warning("Failed to send tapback.") - } - } - } label: { - Text(tb.description) - Image(uiImage: tb.emojiString.image()!) - } - } + Button("Tapback") { + isShowingTapbackInput = true } Button(action: onReply) { diff --git a/Meshtastic/Views/Messages/MessageText.swift b/Meshtastic/Views/Messages/MessageText.swift index 28df8fba..fe332065 100644 --- a/Meshtastic/Views/Messages/MessageText.swift +++ b/Meshtastic/Views/Messages/MessageText.swift @@ -27,13 +27,14 @@ struct MessageText: View { // State for handling channel URL sheet @State private var saveChannelLink: SaveChannelLinkData? @State private var isShowingDeleteConfirmation = false + @State private var isShowingTapbackInput = false + @State private var tapbackText = "" var body: some View { SessionReplayPrivacyView(textAndInputPrivacy: .maskAll) { - let markdownText = LocalizedStringKey(message.messagePayloadMarkdown ?? (message.messagePayload ?? "EMPTY MESSAGE")) - return Text(markdownText) + Text(markdownText) .tint(Self.linkBlue) .padding(.vertical, 10) .padding(.horizontal, 8) @@ -91,6 +92,7 @@ struct MessageText: View { tapBackDestination: tapBackDestination, isCurrentUser: isCurrentUser, isShowingDeleteConfirmation: $isShowingDeleteConfirmation, + isShowingTapbackInput: $isShowingTapbackInput, onReply: onReply ) } @@ -132,6 +134,36 @@ struct MessageText: View { .presentationDetents([.large]) .presentationDragIndicator(.visible) } + .sheet(isPresented: $isShowingTapbackInput) { + TapbackInputView( + text: $tapbackText, + isPresented: $isShowingTapbackInput, + onEmojiSelected: { emoji in + Task { + do { + try await accessoryManager.sendMessage( + message: emoji, + toUserNum: tapBackDestination.userNum, + channel: tapBackDestination.channelNum, + isEmoji: true, + replyID: message.messageId + ) + await MainActor.run { + switch tapBackDestination { + case let .channel(channel): + context.refresh(channel, mergeChanges: true) + case let .user(user): + context.refresh(user, mergeChanges: true) + } + } + } catch { + Logger.services.warning("Failed to send tapback.") + } + } + isShowingTapbackInput = false + } + ) + } .confirmationDialog( "Are you sure you want to delete this message?", isPresented: $isShowingDeleteConfirmation, diff --git a/Meshtastic/Views/Messages/TapbackInputView.swift b/Meshtastic/Views/Messages/TapbackInputView.swift new file mode 100644 index 00000000..bf6de5a6 --- /dev/null +++ b/Meshtastic/Views/Messages/TapbackInputView.swift @@ -0,0 +1,80 @@ +import SwiftUI +import UIKit + +struct TapbackInputView: View { + @Binding var text: String + @Binding var isPresented: Bool + let onEmojiSelected: (String) -> Void + + var body: some View { + NavigationView { + VStack(spacing: 0) { + TextField("Tap to enter emoji", text: $text) + .keyboardType(.emoji) + .frame(height: 50) + .padding(.horizontal) + .background( + RoundedRectangle(cornerRadius: 10) + .strokeBorder(.tertiary, lineWidth: 1) + .background(RoundedRectangle(cornerRadius: 10).fill(Color(.systemBackground))) + ) + .padding(.horizontal) + .padding(.top, 8) + .onChange(of: text) { oldValue, newValue in + // Extract first emoji character and send it + if !newValue.isEmpty, let firstEmoji = extractFirstEmoji(from: newValue) { + onEmojiSelected(firstEmoji) + // Clear the text box after getting the emoji + text = "" + } + } + } + .navigationTitle("Tapback") + .navigationBarTitleDisplayMode(.inline) + .toolbar { + ToolbarItem(placement: .navigationBarTrailing) { + Button("Cancel") { + isPresented = false + } + } + } + } + .presentationDetents([.height(120)]) + } + + private func extractFirstEmoji(from string: String) -> String? { + // Extract the first emoji character(s) - handle both single and multi-scalar emojis + guard !string.isEmpty else { return nil } + + // Try to get the first character + let firstChar = string[string.startIndex] + + // Check if it's an emoji using the existing extension + if firstChar.isEmoji { + // For multi-scalar emojis (like emojis with skin tones), we need to find the full emoji sequence + var emojiEnd = string.index(after: string.startIndex) + + // Check if there are continuation scalars (for emojis with skin tones, variation selectors, etc.) + while emojiEnd < string.endIndex { + let nextChar = string[emojiEnd] + // Check if this is a continuation (variation selector, skin tone modifier, zero-width joiner, etc.) + if let scalar = nextChar.unicodeScalars.first, + (scalar.properties.isVariationSelector || + scalar.value == 0xFE0F || // Variation selector + (scalar.value >= 0x1F3FB && scalar.value <= 0x1F3FF) || // Skin tone modifiers + scalar.value == 0x200D) { // Zero-width joiner + emojiEnd = string.index(after: emojiEnd) + } else if nextChar.isEmoji { + // If it's another emoji, include it (for compound emojis like flags) + emojiEnd = string.index(after: emojiEnd) + } else { + break + } + } + + return String(string[string.startIndex.. = [.medium, .fraction(0.85)] @State private var selectedDetent: PresentationDetent = .medium @State private var waypointFailedAlert: Bool = false @@ -111,26 +110,19 @@ struct WaypointForm: View { HStack { Text("Icon") Spacer() - EmojiOnlyTextField(text: $icon, placeholder: "Select an emoji") + TextField("Select an emoji", text: $icon) + .keyboardType(.emoji) .font(.title) .focused($iconIsFocused) .onChange(of: icon) { _, value in - - // If you have anything other than emojis in your string make it empty - if !value.onlyEmojis() { - icon = "" - } // If a second emoji is entered delete the first one if value.count >= 1 { - if value.count > 1 { let index = value.index(value.startIndex, offsetBy: 1) icon = String(value[index]) } - iconIsFocused = false } } - } Toggle(isOn: $expires) { Label("Expires", systemImage: "clock.badge.xmark") @@ -458,7 +450,6 @@ struct WaypointForm: View { longitude = waypoint.coordinate.longitude } } - .presentationDetents(detents, selection: $selectedDetent) .presentationBackgroundInteraction(.enabled(upThrough: .fraction(0.85))) .presentationDragIndicator(.visible) } diff --git a/Meshtastic/Views/Nodes/MeshMap.swift b/Meshtastic/Views/Nodes/MeshMap.swift index d72987da..3e268afa 100644 --- a/Meshtastic/Views/Nodes/MeshMap.swift +++ b/Meshtastic/Views/Nodes/MeshMap.swift @@ -120,16 +120,14 @@ struct MeshMap: View { } .sheet(item: $selectedWaypoint) { selection in WaypointForm(waypoint: selection) - .padding() + .presentationDetents([.large]) } .sheet(item: $editingWaypoint) { selection in WaypointForm(waypoint: selection, editMode: true) - .padding() + .presentationDetents([.large]) } .sheet(isPresented: $editingSettings) { MapSettingsForm(traffic: $showTraffic, pointsOfInterest: $showPointsOfInterest, mapLayer: $selectedMapLayer, meshMap: $isMeshMap, enabledOverlayConfigs: $enabledOverlayConfigs) - .presentationDetents([.large]) - } .onChange(of: router.navigationState) { guard case .map = router.navigationState.selectedTab else { return } diff --git a/Meshtastic/Views/Settings/AppSettings.swift b/Meshtastic/Views/Settings/AppSettings.swift index 2f10c2af..243325b1 100644 --- a/Meshtastic/Views/Settings/AppSettings.swift +++ b/Meshtastic/Views/Settings/AppSettings.swift @@ -120,7 +120,7 @@ struct AppSettings: View { Text("180") } } - Text("Favorited and ignored nodes are always retained. Nodes without PKC keys are cleared from the app database on the schedule set by the user, nodes with PKC keys are cleared only if the interval is set to 7 days or longer. This feature only purges nodes from the app that are not stored in the device node database.") + Text("Favorited and ignored nodes are always retained. Other nodes are cleared from the app database on the schedule set by the user. (Nodes with PKC keys are always retained for at least 7 days.) This feature only purges nodes from the app that are not stored in the device node database.") .foregroundStyle(.secondary) .font(idiom == .phone ? .caption : .callout) } diff --git a/MeshtasticProtobufs/Sources/meshtastic/admin.pb.swift b/MeshtasticProtobufs/Sources/meshtastic/admin.pb.swift index fa6a9e61..0826bc64 100644 --- a/MeshtasticProtobufs/Sources/meshtastic/admin.pb.swift +++ b/MeshtasticProtobufs/Sources/meshtastic/admin.pb.swift @@ -21,6 +21,55 @@ fileprivate struct _GeneratedWithProtocGenSwiftVersion: SwiftProtobuf.ProtobufAP typealias Version = _2 } +/// +/// Firmware update mode for OTA updates +public enum OTAMode: SwiftProtobuf.Enum, Swift.CaseIterable { + public typealias RawValue = Int + + /// + /// Do not reboot into OTA mode + case noRebootOta // = 0 + + /// + /// Reboot into OTA mode for BLE firmware update + case otaBle // = 1 + + /// + /// Reboot into OTA mode for WiFi firmware update + case otaWifi // = 2 + case UNRECOGNIZED(Int) + + public init() { + self = .noRebootOta + } + + public init?(rawValue: Int) { + switch rawValue { + case 0: self = .noRebootOta + case 1: self = .otaBle + case 2: self = .otaWifi + default: self = .UNRECOGNIZED(rawValue) + } + } + + public var rawValue: Int { + switch self { + case .noRebootOta: return 0 + case .otaBle: return 1 + case .otaWifi: return 2 + case .UNRECOGNIZED(let i): return i + } + } + + // The compiler won't synthesize support with the UNRECOGNIZED case. + public static let allCases: [OTAMode] = [ + .noRebootOta, + .otaBle, + .otaWifi, + ] + +} + /// /// This message is handled by the Admin module and is responsible for all settings/channel read/write operations. /// This message is used to do settings operations to both remote AND local nodes. @@ -532,6 +581,9 @@ public struct AdminMessage: Sendable { /// /// Tell the node to reboot into the OTA Firmware in this many seconds (or <0 to cancel reboot) /// Only Implemented for ESP32 Devices. This needs to be issued to send a new main firmware via bluetooth. + /// Deprecated in favor of reboot_ota_mode in 2.7.17 + /// + /// NOTE: This field was marked as deprecated in the .proto file. public var rebootOtaSeconds: Int32 { get { if case .rebootOtaSeconds(let v)? = payloadVariant {return v} @@ -592,6 +644,16 @@ public struct AdminMessage: Sendable { set {payloadVariant = .nodedbReset(newValue)} } + /// + /// Tell the node to reset into the OTA Loader + public var otaRequest: AdminMessage.OTAEvent { + get { + if case .otaRequest(let v)? = payloadVariant {return v} + return AdminMessage.OTAEvent() + } + set {payloadVariant = .otaRequest(newValue)} + } + public var unknownFields = SwiftProtobuf.UnknownStorage() /// @@ -753,6 +815,9 @@ public struct AdminMessage: Sendable { /// /// Tell the node to reboot into the OTA Firmware in this many seconds (or <0 to cancel reboot) /// Only Implemented for ESP32 Devices. This needs to be issued to send a new main firmware via bluetooth. + /// Deprecated in favor of reboot_ota_mode in 2.7.17 + /// + /// NOTE: This field was marked as deprecated in the .proto file. case rebootOtaSeconds(Int32) /// /// This message is only supported for the simulator Portduino build. @@ -771,6 +836,9 @@ public struct AdminMessage: Sendable { /// Tell the node to reset the nodedb. /// When true, favorites are preserved through reset. case nodedbReset(Bool) + /// + /// Tell the node to reset into the OTA Loader + case otaRequest(AdminMessage.OTAEvent) } @@ -1059,6 +1127,28 @@ public struct AdminMessage: Sendable { public init() {} } + /// + /// User is requesting an over the air update. + /// Node will reboot into the OTA loader + public struct OTAEvent: Sendable { + // SwiftProtobuf.Message conformance is added in an extension below. See the + // `Message` and `Message+*Additions` files in the SwiftProtobuf library for + // methods supported on all messages. + + /// + /// Tell the node to reboot into OTA mode for firmware update via BLE or WiFi (ESP32 only for now) + public var rebootOtaMode: OTAMode = .noRebootOta + + /// + /// A 32 byte hash of the OTA firmware. + /// Used to verify the integrity of the firmware before applying an update. + public var otaHash: Data = Data() + + public var unknownFields = SwiftProtobuf.UnknownStorage() + + public init() {} + } + public init() {} } @@ -1239,9 +1329,13 @@ public struct KeyVerificationAdmin: Sendable { fileprivate let _protobuf_package = "meshtastic" +extension OTAMode: SwiftProtobuf._ProtoNameProviding { + public static let _protobuf_nameMap = SwiftProtobuf._NameMap(bytecode: "\0\u{2}\0NO_REBOOT_OTA\0\u{1}OTA_BLE\0\u{1}OTA_WIFI\0") +} + extension AdminMessage: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { public static let protoMessageName: String = _protobuf_package + ".AdminMessage" - public static let _protobuf_nameMap = SwiftProtobuf._NameMap(bytecode: "\0\u{3}get_channel_request\0\u{3}get_channel_response\0\u{3}get_owner_request\0\u{3}get_owner_response\0\u{3}get_config_request\0\u{3}get_config_response\0\u{3}get_module_config_request\0\u{3}get_module_config_response\0\u{4}\u{2}get_canned_message_module_messages_request\0\u{3}get_canned_message_module_messages_response\0\u{3}get_device_metadata_request\0\u{3}get_device_metadata_response\0\u{3}get_ringtone_request\0\u{3}get_ringtone_response\0\u{3}get_device_connection_status_request\0\u{3}get_device_connection_status_response\0\u{3}set_ham_mode\0\u{3}get_node_remote_hardware_pins_request\0\u{3}get_node_remote_hardware_pins_response\0\u{3}enter_dfu_mode_request\0\u{3}delete_file_request\0\u{3}set_scale\0\u{3}backup_preferences\0\u{3}restore_preferences\0\u{3}remove_backup_preferences\0\u{3}send_input_event\0\u{4}\u{5}set_owner\0\u{3}set_channel\0\u{3}set_config\0\u{3}set_module_config\0\u{3}set_canned_message_module_messages\0\u{3}set_ringtone_message\0\u{3}remove_by_nodenum\0\u{3}set_favorite_node\0\u{3}remove_favorite_node\0\u{3}set_fixed_position\0\u{3}remove_fixed_position\0\u{3}set_time_only\0\u{3}get_ui_config_request\0\u{3}get_ui_config_response\0\u{3}store_ui_config\0\u{3}set_ignored_node\0\u{3}remove_ignored_node\0\u{4}\u{10}begin_edit_settings\0\u{3}commit_edit_settings\0\u{3}add_contact\0\u{3}key_verification\0\u{4}\u{1b}factory_reset_device\0\u{3}reboot_ota_seconds\0\u{3}exit_simulator\0\u{3}reboot_seconds\0\u{3}shutdown_seconds\0\u{3}factory_reset_config\0\u{3}nodedb_reset\0\u{3}session_passkey\0") + public static let _protobuf_nameMap = SwiftProtobuf._NameMap(bytecode: "\0\u{3}get_channel_request\0\u{3}get_channel_response\0\u{3}get_owner_request\0\u{3}get_owner_response\0\u{3}get_config_request\0\u{3}get_config_response\0\u{3}get_module_config_request\0\u{3}get_module_config_response\0\u{4}\u{2}get_canned_message_module_messages_request\0\u{3}get_canned_message_module_messages_response\0\u{3}get_device_metadata_request\0\u{3}get_device_metadata_response\0\u{3}get_ringtone_request\0\u{3}get_ringtone_response\0\u{3}get_device_connection_status_request\0\u{3}get_device_connection_status_response\0\u{3}set_ham_mode\0\u{3}get_node_remote_hardware_pins_request\0\u{3}get_node_remote_hardware_pins_response\0\u{3}enter_dfu_mode_request\0\u{3}delete_file_request\0\u{3}set_scale\0\u{3}backup_preferences\0\u{3}restore_preferences\0\u{3}remove_backup_preferences\0\u{3}send_input_event\0\u{4}\u{5}set_owner\0\u{3}set_channel\0\u{3}set_config\0\u{3}set_module_config\0\u{3}set_canned_message_module_messages\0\u{3}set_ringtone_message\0\u{3}remove_by_nodenum\0\u{3}set_favorite_node\0\u{3}remove_favorite_node\0\u{3}set_fixed_position\0\u{3}remove_fixed_position\0\u{3}set_time_only\0\u{3}get_ui_config_request\0\u{3}get_ui_config_response\0\u{3}store_ui_config\0\u{3}set_ignored_node\0\u{3}remove_ignored_node\0\u{4}\u{10}begin_edit_settings\0\u{3}commit_edit_settings\0\u{3}add_contact\0\u{3}key_verification\0\u{4}\u{1b}factory_reset_device\0\u{3}reboot_ota_seconds\0\u{3}exit_simulator\0\u{3}reboot_seconds\0\u{3}shutdown_seconds\0\u{3}factory_reset_config\0\u{3}nodedb_reset\0\u{3}session_passkey\0\u{3}ota_request\0") public mutating func decodeMessage(decoder: inout D) throws { while let fieldNumber = try decoder.nextFieldNumber() { @@ -1772,6 +1866,19 @@ extension AdminMessage: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementat } }() case 101: try { try decoder.decodeSingularBytesField(value: &self.sessionPasskey) }() + case 102: try { + var v: AdminMessage.OTAEvent? + var hadOneofValue = false + if let current = self.payloadVariant { + hadOneofValue = true + if case .otaRequest(let m) = current {v = m} + } + try decoder.decodeSingularMessageField(value: &v) + if let v = v { + if hadOneofValue {try decoder.handleConflictingOneOf()} + self.payloadVariant = .otaRequest(v) + } + }() default: break } } @@ -1999,11 +2106,14 @@ extension AdminMessage: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementat guard case .nodedbReset(let v)? = self.payloadVariant else { preconditionFailure() } try visitor.visitSingularBoolField(value: v, fieldNumber: 100) }() - case nil: break + default: break } if !self.sessionPasskey.isEmpty { try visitor.visitSingularBytesField(value: self.sessionPasskey, fieldNumber: 101) } + try { if case .otaRequest(let v)? = self.payloadVariant { + try visitor.visitSingularMessageField(value: v, fieldNumber: 102) + } }() try unknownFields.traverse(visitor: &visitor) } @@ -2072,6 +2182,41 @@ extension AdminMessage.InputEvent: SwiftProtobuf.Message, SwiftProtobuf._Message } } +extension AdminMessage.OTAEvent: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { + public static let protoMessageName: String = AdminMessage.protoMessageName + ".OTAEvent" + public static let _protobuf_nameMap = SwiftProtobuf._NameMap(bytecode: "\0\u{3}reboot_ota_mode\0\u{3}ota_hash\0") + + public mutating func decodeMessage(decoder: inout D) throws { + while let fieldNumber = try decoder.nextFieldNumber() { + // The use of inline closures is to circumvent an issue where the compiler + // allocates stack space for every case branch when no optimizations are + // enabled. https://github.com/apple/swift-protobuf/issues/1034 + switch fieldNumber { + case 1: try { try decoder.decodeSingularEnumField(value: &self.rebootOtaMode) }() + case 2: try { try decoder.decodeSingularBytesField(value: &self.otaHash) }() + default: break + } + } + } + + public func traverse(visitor: inout V) throws { + if self.rebootOtaMode != .noRebootOta { + try visitor.visitSingularEnumField(value: self.rebootOtaMode, fieldNumber: 1) + } + if !self.otaHash.isEmpty { + try visitor.visitSingularBytesField(value: self.otaHash, fieldNumber: 2) + } + try unknownFields.traverse(visitor: &visitor) + } + + public static func ==(lhs: AdminMessage.OTAEvent, rhs: AdminMessage.OTAEvent) -> Bool { + if lhs.rebootOtaMode != rhs.rebootOtaMode {return false} + if lhs.otaHash != rhs.otaHash {return false} + if lhs.unknownFields != rhs.unknownFields {return false} + return true + } +} + extension HamParameters: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { public static let protoMessageName: String = _protobuf_package + ".HamParameters" public static let _protobuf_nameMap = SwiftProtobuf._NameMap(bytecode: "\0\u{3}call_sign\0\u{3}tx_power\0\u{1}frequency\0\u{3}short_name\0") diff --git a/MeshtasticProtobufs/Sources/meshtastic/mesh.pb.swift b/MeshtasticProtobufs/Sources/meshtastic/mesh.pb.swift index 477e2457..99f15718 100644 --- a/MeshtasticProtobufs/Sources/meshtastic/mesh.pb.swift +++ b/MeshtasticProtobufs/Sources/meshtastic/mesh.pb.swift @@ -166,8 +166,8 @@ public enum HardwareModel: SwiftProtobuf.Enum, Swift.CaseIterable { case loraRelayV1 // = 32 /// - /// TODO: REPLACE - case nrf52840Dk // = 33 + /// T-Echo Plus device from LilyGo + case tEchoPlus // = 33 /// /// TODO: REPLACE @@ -535,6 +535,10 @@ public enum HardwareModel: SwiftProtobuf.Enum, Swift.CaseIterable { /// Elecrow ThinkNode M6 case thinknodeM6 // = 120 + /// + /// Elecrow Meshstick 1262 + case meshstick1262 // = 121 + /// /// ------------------------------------------------------------------------------------------------------------------------------------------ /// Reserved ID For developing private Ports. These will show up in live traffic sparsely, so we can use a high number. Keep it within 8 bits. @@ -581,7 +585,7 @@ public enum HardwareModel: SwiftProtobuf.Enum, Swift.CaseIterable { case 30: self = .rp2040Lora case 31: self = .stationG2 case 32: self = .loraRelayV1 - case 33: self = .nrf52840Dk + case 33: self = .tEchoPlus case 34: self = .ppr case 35: self = .genieblocks case 36: self = .nrf52Unknown @@ -669,6 +673,7 @@ public enum HardwareModel: SwiftProtobuf.Enum, Swift.CaseIterable { case 118: self = .rak6421 case 119: self = .thinknodeM4 case 120: self = .thinknodeM6 + case 121: self = .meshstick1262 case 255: self = .privateHw default: self = .UNRECOGNIZED(rawValue) } @@ -709,7 +714,7 @@ public enum HardwareModel: SwiftProtobuf.Enum, Swift.CaseIterable { case .rp2040Lora: return 30 case .stationG2: return 31 case .loraRelayV1: return 32 - case .nrf52840Dk: return 33 + case .tEchoPlus: return 33 case .ppr: return 34 case .genieblocks: return 35 case .nrf52Unknown: return 36 @@ -797,6 +802,7 @@ public enum HardwareModel: SwiftProtobuf.Enum, Swift.CaseIterable { case .rak6421: return 118 case .thinknodeM4: return 119 case .thinknodeM6: return 120 + case .meshstick1262: return 121 case .privateHw: return 255 case .UNRECOGNIZED(let i): return i } @@ -837,7 +843,7 @@ public enum HardwareModel: SwiftProtobuf.Enum, Swift.CaseIterable { .rp2040Lora, .stationG2, .loraRelayV1, - .nrf52840Dk, + .tEchoPlus, .ppr, .genieblocks, .nrf52Unknown, @@ -925,6 +931,7 @@ public enum HardwareModel: SwiftProtobuf.Enum, Swift.CaseIterable { .rak6421, .thinknodeM4, .thinknodeM6, + .meshstick1262, .privateHw, ] @@ -1921,6 +1928,11 @@ public struct Routing: Sendable { /// Airtime fairness rate limit exceeded for a packet /// This typically enforced per portnum and is used to prevent a single node from monopolizing airtime case rateLimitExceeded // = 38 + + /// + /// PKI encryption failed, due to no public key for the remote node + /// This is different from PKI_UNKNOWN_PUBKEY which indicates a failure upon receiving a packet + case pkiSendFailPublicKey // = 39 case UNRECOGNIZED(Int) public init() { @@ -1946,6 +1958,7 @@ public struct Routing: Sendable { case 36: self = .adminBadSessionKey case 37: self = .adminPublicKeyUnauthorized case 38: self = .rateLimitExceeded + case 39: self = .pkiSendFailPublicKey default: self = .UNRECOGNIZED(rawValue) } } @@ -1969,6 +1982,7 @@ public struct Routing: Sendable { case .adminBadSessionKey: return 36 case .adminPublicKeyUnauthorized: return 37 case .rateLimitExceeded: return 38 + case .pkiSendFailPublicKey: return 39 case .UNRECOGNIZED(let i): return i } } @@ -1992,6 +2006,7 @@ public struct Routing: Sendable { .adminBadSessionKey, .adminPublicKeyUnauthorized, .rateLimitExceeded, + .pkiSendFailPublicKey, ] } @@ -2136,6 +2151,10 @@ public struct StoreForwardPlusPlus: Sendable { /// The receive time of the message in question public var encapsulatedRxtime: UInt32 = 0 + /// + /// Used in a LINK_REQUEST to specify the message X spots back from head + public var chainCount: UInt32 = 0 + public var unknownFields = SwiftProtobuf.UnknownStorage() /// @@ -3959,7 +3978,7 @@ public struct ChunkedPayloadResponse: Sendable { fileprivate let _protobuf_package = "meshtastic" extension HardwareModel: SwiftProtobuf._ProtoNameProviding { - public static let _protobuf_nameMap = SwiftProtobuf._NameMap(bytecode: "\0\u{2}\0UNSET\0\u{1}TLORA_V2\0\u{1}TLORA_V1\0\u{1}TLORA_V2_1_1P6\0\u{1}TBEAM\0\u{1}HELTEC_V2_0\0\u{1}TBEAM_V0P7\0\u{1}T_ECHO\0\u{1}TLORA_V1_1P3\0\u{1}RAK4631\0\u{1}HELTEC_V2_1\0\u{1}HELTEC_V1\0\u{1}LILYGO_TBEAM_S3_CORE\0\u{1}RAK11200\0\u{1}NANO_G1\0\u{1}TLORA_V2_1_1P8\0\u{1}TLORA_T3_S3\0\u{1}NANO_G1_EXPLORER\0\u{1}NANO_G2_ULTRA\0\u{1}LORA_TYPE\0\u{1}WIPHONE\0\u{1}WIO_WM1110\0\u{1}RAK2560\0\u{1}HELTEC_HRU_3601\0\u{1}HELTEC_WIRELESS_BRIDGE\0\u{1}STATION_G1\0\u{1}RAK11310\0\u{1}SENSELORA_RP2040\0\u{1}SENSELORA_S3\0\u{1}CANARYONE\0\u{1}RP2040_LORA\0\u{1}STATION_G2\0\u{1}LORA_RELAY_V1\0\u{1}NRF52840DK\0\u{1}PPR\0\u{1}GENIEBLOCKS\0\u{1}NRF52_UNKNOWN\0\u{1}PORTDUINO\0\u{1}ANDROID_SIM\0\u{1}DIY_V1\0\u{1}NRF52840_PCA10059\0\u{1}DR_DEV\0\u{1}M5STACK\0\u{1}HELTEC_V3\0\u{1}HELTEC_WSL_V3\0\u{1}BETAFPV_2400_TX\0\u{1}BETAFPV_900_NANO_TX\0\u{1}RPI_PICO\0\u{1}HELTEC_WIRELESS_TRACKER\0\u{1}HELTEC_WIRELESS_PAPER\0\u{1}T_DECK\0\u{1}T_WATCH_S3\0\u{1}PICOMPUTER_S3\0\u{1}HELTEC_HT62\0\u{1}EBYTE_ESP32_S3\0\u{1}ESP32_S3_PICO\0\u{1}CHATTER_2\0\u{1}HELTEC_WIRELESS_PAPER_V1_0\0\u{1}HELTEC_WIRELESS_TRACKER_V1_0\0\u{1}UNPHONE\0\u{1}TD_LORAC\0\u{1}CDEBYTE_EORA_S3\0\u{1}TWC_MESH_V4\0\u{1}NRF52_PROMICRO_DIY\0\u{1}RADIOMASTER_900_BANDIT_NANO\0\u{1}HELTEC_CAPSULE_SENSOR_V3\0\u{1}HELTEC_VISION_MASTER_T190\0\u{1}HELTEC_VISION_MASTER_E213\0\u{1}HELTEC_VISION_MASTER_E290\0\u{1}HELTEC_MESH_NODE_T114\0\u{1}SENSECAP_INDICATOR\0\u{1}TRACKER_T1000_E\0\u{1}RAK3172\0\u{1}WIO_E5\0\u{1}RADIOMASTER_900_BANDIT\0\u{1}ME25LS01_4Y10TD\0\u{1}RP2040_FEATHER_RFM95\0\u{1}M5STACK_COREBASIC\0\u{1}M5STACK_CORE2\0\u{1}RPI_PICO2\0\u{1}M5STACK_CORES3\0\u{1}SEEED_XIAO_S3\0\u{1}MS24SF1\0\u{1}TLORA_C6\0\u{1}WISMESH_TAP\0\u{1}ROUTASTIC\0\u{1}MESH_TAB\0\u{1}MESHLINK\0\u{1}XIAO_NRF52_KIT\0\u{1}THINKNODE_M1\0\u{1}THINKNODE_M2\0\u{1}T_ETH_ELITE\0\u{1}HELTEC_SENSOR_HUB\0\u{1}MUZI_BASE\0\u{1}HELTEC_MESH_POCKET\0\u{1}SEEED_SOLAR_NODE\0\u{1}NOMADSTAR_METEOR_PRO\0\u{1}CROWPANEL\0\u{1}LINK_32\0\u{1}SEEED_WIO_TRACKER_L1\0\u{1}SEEED_WIO_TRACKER_L1_EINK\0\u{1}MUZI_R1_NEO\0\u{1}T_DECK_PRO\0\u{1}T_LORA_PAGER\0\u{1}M5STACK_RESERVED\0\u{1}WISMESH_TAG\0\u{1}RAK3312\0\u{1}THINKNODE_M5\0\u{1}HELTEC_MESH_SOLAR\0\u{1}T_ECHO_LITE\0\u{1}HELTEC_V4\0\u{1}M5STACK_C6L\0\u{1}M5STACK_CARDPUTER_ADV\0\u{1}HELTEC_WIRELESS_TRACKER_V2\0\u{1}T_WATCH_ULTRA\0\u{1}THINKNODE_M3\0\u{1}WISMESH_TAP_V2\0\u{1}RAK3401\0\u{1}RAK6421\0\u{1}THINKNODE_M4\0\u{1}THINKNODE_M6\0\u{2}G\u{2}PRIVATE_HW\0") + public static let _protobuf_nameMap = SwiftProtobuf._NameMap(bytecode: "\0\u{2}\0UNSET\0\u{1}TLORA_V2\0\u{1}TLORA_V1\0\u{1}TLORA_V2_1_1P6\0\u{1}TBEAM\0\u{1}HELTEC_V2_0\0\u{1}TBEAM_V0P7\0\u{1}T_ECHO\0\u{1}TLORA_V1_1P3\0\u{1}RAK4631\0\u{1}HELTEC_V2_1\0\u{1}HELTEC_V1\0\u{1}LILYGO_TBEAM_S3_CORE\0\u{1}RAK11200\0\u{1}NANO_G1\0\u{1}TLORA_V2_1_1P8\0\u{1}TLORA_T3_S3\0\u{1}NANO_G1_EXPLORER\0\u{1}NANO_G2_ULTRA\0\u{1}LORA_TYPE\0\u{1}WIPHONE\0\u{1}WIO_WM1110\0\u{1}RAK2560\0\u{1}HELTEC_HRU_3601\0\u{1}HELTEC_WIRELESS_BRIDGE\0\u{1}STATION_G1\0\u{1}RAK11310\0\u{1}SENSELORA_RP2040\0\u{1}SENSELORA_S3\0\u{1}CANARYONE\0\u{1}RP2040_LORA\0\u{1}STATION_G2\0\u{1}LORA_RELAY_V1\0\u{1}T_ECHO_PLUS\0\u{1}PPR\0\u{1}GENIEBLOCKS\0\u{1}NRF52_UNKNOWN\0\u{1}PORTDUINO\0\u{1}ANDROID_SIM\0\u{1}DIY_V1\0\u{1}NRF52840_PCA10059\0\u{1}DR_DEV\0\u{1}M5STACK\0\u{1}HELTEC_V3\0\u{1}HELTEC_WSL_V3\0\u{1}BETAFPV_2400_TX\0\u{1}BETAFPV_900_NANO_TX\0\u{1}RPI_PICO\0\u{1}HELTEC_WIRELESS_TRACKER\0\u{1}HELTEC_WIRELESS_PAPER\0\u{1}T_DECK\0\u{1}T_WATCH_S3\0\u{1}PICOMPUTER_S3\0\u{1}HELTEC_HT62\0\u{1}EBYTE_ESP32_S3\0\u{1}ESP32_S3_PICO\0\u{1}CHATTER_2\0\u{1}HELTEC_WIRELESS_PAPER_V1_0\0\u{1}HELTEC_WIRELESS_TRACKER_V1_0\0\u{1}UNPHONE\0\u{1}TD_LORAC\0\u{1}CDEBYTE_EORA_S3\0\u{1}TWC_MESH_V4\0\u{1}NRF52_PROMICRO_DIY\0\u{1}RADIOMASTER_900_BANDIT_NANO\0\u{1}HELTEC_CAPSULE_SENSOR_V3\0\u{1}HELTEC_VISION_MASTER_T190\0\u{1}HELTEC_VISION_MASTER_E213\0\u{1}HELTEC_VISION_MASTER_E290\0\u{1}HELTEC_MESH_NODE_T114\0\u{1}SENSECAP_INDICATOR\0\u{1}TRACKER_T1000_E\0\u{1}RAK3172\0\u{1}WIO_E5\0\u{1}RADIOMASTER_900_BANDIT\0\u{1}ME25LS01_4Y10TD\0\u{1}RP2040_FEATHER_RFM95\0\u{1}M5STACK_COREBASIC\0\u{1}M5STACK_CORE2\0\u{1}RPI_PICO2\0\u{1}M5STACK_CORES3\0\u{1}SEEED_XIAO_S3\0\u{1}MS24SF1\0\u{1}TLORA_C6\0\u{1}WISMESH_TAP\0\u{1}ROUTASTIC\0\u{1}MESH_TAB\0\u{1}MESHLINK\0\u{1}XIAO_NRF52_KIT\0\u{1}THINKNODE_M1\0\u{1}THINKNODE_M2\0\u{1}T_ETH_ELITE\0\u{1}HELTEC_SENSOR_HUB\0\u{1}MUZI_BASE\0\u{1}HELTEC_MESH_POCKET\0\u{1}SEEED_SOLAR_NODE\0\u{1}NOMADSTAR_METEOR_PRO\0\u{1}CROWPANEL\0\u{1}LINK_32\0\u{1}SEEED_WIO_TRACKER_L1\0\u{1}SEEED_WIO_TRACKER_L1_EINK\0\u{1}MUZI_R1_NEO\0\u{1}T_DECK_PRO\0\u{1}T_LORA_PAGER\0\u{1}M5STACK_RESERVED\0\u{1}WISMESH_TAG\0\u{1}RAK3312\0\u{1}THINKNODE_M5\0\u{1}HELTEC_MESH_SOLAR\0\u{1}T_ECHO_LITE\0\u{1}HELTEC_V4\0\u{1}M5STACK_C6L\0\u{1}M5STACK_CARDPUTER_ADV\0\u{1}HELTEC_WIRELESS_TRACKER_V2\0\u{1}T_WATCH_ULTRA\0\u{1}THINKNODE_M3\0\u{1}WISMESH_TAP_V2\0\u{1}RAK3401\0\u{1}RAK6421\0\u{1}THINKNODE_M4\0\u{1}THINKNODE_M6\0\u{1}MESHSTICK_1262\0\u{2}F\u{2}PRIVATE_HW\0") } extension Constants: SwiftProtobuf._ProtoNameProviding { @@ -4409,7 +4428,7 @@ extension Routing: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBa } extension Routing.Error: SwiftProtobuf._ProtoNameProviding { - public static let _protobuf_nameMap = SwiftProtobuf._NameMap(bytecode: "\0\u{2}\0NONE\0\u{1}NO_ROUTE\0\u{1}GOT_NAK\0\u{1}TIMEOUT\0\u{1}NO_INTERFACE\0\u{1}MAX_RETRANSMIT\0\u{1}NO_CHANNEL\0\u{1}TOO_LARGE\0\u{1}NO_RESPONSE\0\u{1}DUTY_CYCLE_LIMIT\0\u{2}\u{17}BAD_REQUEST\0\u{1}NOT_AUTHORIZED\0\u{1}PKI_FAILED\0\u{1}PKI_UNKNOWN_PUBKEY\0\u{1}ADMIN_BAD_SESSION_KEY\0\u{1}ADMIN_PUBLIC_KEY_UNAUTHORIZED\0\u{1}RATE_LIMIT_EXCEEDED\0") + public static let _protobuf_nameMap = SwiftProtobuf._NameMap(bytecode: "\0\u{2}\0NONE\0\u{1}NO_ROUTE\0\u{1}GOT_NAK\0\u{1}TIMEOUT\0\u{1}NO_INTERFACE\0\u{1}MAX_RETRANSMIT\0\u{1}NO_CHANNEL\0\u{1}TOO_LARGE\0\u{1}NO_RESPONSE\0\u{1}DUTY_CYCLE_LIMIT\0\u{2}\u{17}BAD_REQUEST\0\u{1}NOT_AUTHORIZED\0\u{1}PKI_FAILED\0\u{1}PKI_UNKNOWN_PUBKEY\0\u{1}ADMIN_BAD_SESSION_KEY\0\u{1}ADMIN_PUBLIC_KEY_UNAUTHORIZED\0\u{1}RATE_LIMIT_EXCEEDED\0\u{1}PKI_SEND_FAIL_PUBLIC_KEY\0") } extension DataMessage: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { @@ -4528,7 +4547,7 @@ extension KeyVerification: SwiftProtobuf.Message, SwiftProtobuf._MessageImplemen extension StoreForwardPlusPlus: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { public static let protoMessageName: String = _protobuf_package + ".StoreForwardPlusPlus" - public static let _protobuf_nameMap = SwiftProtobuf._NameMap(bytecode: "\0\u{3}sfpp_message_type\0\u{3}message_hash\0\u{3}commit_hash\0\u{3}root_hash\0\u{1}message\0\u{3}encapsulated_id\0\u{3}encapsulated_to\0\u{3}encapsulated_from\0\u{3}encapsulated_rxtime\0") + public static let _protobuf_nameMap = SwiftProtobuf._NameMap(bytecode: "\0\u{3}sfpp_message_type\0\u{3}message_hash\0\u{3}commit_hash\0\u{3}root_hash\0\u{1}message\0\u{3}encapsulated_id\0\u{3}encapsulated_to\0\u{3}encapsulated_from\0\u{3}encapsulated_rxtime\0\u{3}chain_count\0") public mutating func decodeMessage(decoder: inout D) throws { while let fieldNumber = try decoder.nextFieldNumber() { @@ -4545,6 +4564,7 @@ extension StoreForwardPlusPlus: SwiftProtobuf.Message, SwiftProtobuf._MessageImp case 7: try { try decoder.decodeSingularUInt32Field(value: &self.encapsulatedTo) }() case 8: try { try decoder.decodeSingularUInt32Field(value: &self.encapsulatedFrom) }() case 9: try { try decoder.decodeSingularUInt32Field(value: &self.encapsulatedRxtime) }() + case 10: try { try decoder.decodeSingularUInt32Field(value: &self.chainCount) }() default: break } } @@ -4578,6 +4598,9 @@ extension StoreForwardPlusPlus: SwiftProtobuf.Message, SwiftProtobuf._MessageImp if self.encapsulatedRxtime != 0 { try visitor.visitSingularUInt32Field(value: self.encapsulatedRxtime, fieldNumber: 9) } + if self.chainCount != 0 { + try visitor.visitSingularUInt32Field(value: self.chainCount, fieldNumber: 10) + } try unknownFields.traverse(visitor: &visitor) } @@ -4591,6 +4614,7 @@ extension StoreForwardPlusPlus: SwiftProtobuf.Message, SwiftProtobuf._MessageImp if lhs.encapsulatedTo != rhs.encapsulatedTo {return false} if lhs.encapsulatedFrom != rhs.encapsulatedFrom {return false} if lhs.encapsulatedRxtime != rhs.encapsulatedRxtime {return false} + if lhs.chainCount != rhs.chainCount {return false} if lhs.unknownFields != rhs.unknownFields {return false} return true } diff --git a/protobufs b/protobufs index 62ef17b3..4ed2d1a3 160000 --- a/protobufs +++ b/protobufs @@ -1 +1 @@ -Subproject commit 62ef17b3d1625fc6d78ed661f614d0baad4be9ef +Subproject commit 4ed2d1a35e7f486708ead6d06fb2597c9aa87245 From 088f635c9f8094fa3cec32e570aa238f9e1f88c4 Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Thu, 15 Jan 2026 09:43:43 -0800 Subject: [PATCH 2/4] Revert "Make BLE Transport an actor to fix background discovery crashes (#1554)" (#1560) This reverts commit bc39cbd2b77e9e9da024af4c6e38c35254af2080. --- Localizable.xcstrings | 12 +- Meshtastic.xcodeproj/project.pbxproj | 20 +-- .../AccessoryManager+Connect.swift | 1 + .../AccessoryManager+Discovery.swift | 2 +- .../AccessoryManager+ToRadio.swift | 78 ++++----- .../Accessory Manager/AccessoryManager.swift | 2 - .../Accessory/Protocols/Connection.swift | 2 +- .../Accessory/Protocols/Transport.swift | 4 +- .../Bluetooth Low Energy/BLEConnection.swift | 10 +- .../Bluetooth Low Energy/BLETransport.swift | 86 ++++------ Meshtastic/Extensions/UIKeyboardType.swift | 13 -- Meshtastic/Extensions/UserDefaults.swift | 4 - Meshtastic/Helpers/EmojiOnlyTextField.swift | 46 ------ Meshtastic/Helpers/MeshPackets.swift | 4 +- .../Messages/MessageContextMenuItems.swift | 27 +++- Meshtastic/Views/Messages/MessageText.swift | 36 +---- .../Views/Messages/TapbackInputView.swift | 80 ---------- .../Nodes/Helpers/Map/WaypointForm.swift | 13 +- Meshtastic/Views/Nodes/MeshMap.swift | 6 +- Meshtastic/Views/Settings/AppSettings.swift | 2 +- .../Sources/meshtastic/admin.pb.swift | 149 +----------------- .../Sources/meshtastic/mesh.pb.swift | 40 +---- protobufs | 2 +- 23 files changed, 140 insertions(+), 499 deletions(-) delete mode 100644 Meshtastic/Extensions/UIKeyboardType.swift delete mode 100644 Meshtastic/Views/Messages/TapbackInputView.swift diff --git a/Localizable.xcstrings b/Localizable.xcstrings index bc642a1a..33be032d 100644 --- a/Localizable.xcstrings +++ b/Localizable.xcstrings @@ -14315,7 +14315,6 @@ } }, "Favorited and ignored nodes are always retained. Nodes without PKC keys are cleared from the app database on the schedule set by the user, nodes with PKC keys are cleared only if the interval is set to 7 days or longer. This feature only purges nodes from the app that are not stored in the device node database." : { - "extractionState" : "stale", "localizations" : { "de" : { "stringUnit" : { @@ -14336,9 +14335,6 @@ } } } - }, - "Favorited and ignored nodes are always retained. Other nodes are cleared from the app database on the schedule set by the user. (Nodes with PKC keys are always retained for at least 7 days.) This feature only purges nodes from the app that are not stored in the device node database." : { - }, "Favorites" : { "localizations" : { @@ -31621,9 +31617,6 @@ } } } - }, - "Select an emoji" : { - }, "Select Channel" : { "localizations" : { @@ -35554,9 +35547,6 @@ } } } - }, - "Tap to enter emoji" : { - }, "Tapback" : { "localizations" : { @@ -42331,4 +42321,4 @@ } }, "version" : "1.1" -} \ No newline at end of file +} diff --git a/Meshtastic.xcodeproj/project.pbxproj b/Meshtastic.xcodeproj/project.pbxproj index cc68584c..b87ce5f2 100644 --- a/Meshtastic.xcodeproj/project.pbxproj +++ b/Meshtastic.xcodeproj/project.pbxproj @@ -115,7 +115,6 @@ D93068D52B812B700066FBC8 /* MessageDestination.swift in Sources */ = {isa = PBXBuildFile; fileRef = D93068D42B812B700066FBC8 /* MessageDestination.swift */; }; D93068D72B8146690066FBC8 /* MessageText.swift in Sources */ = {isa = PBXBuildFile; fileRef = D93068D62B8146690066FBC8 /* MessageText.swift */; }; D93068D92B81509C0066FBC8 /* TapbackResponses.swift in Sources */ = {isa = PBXBuildFile; fileRef = D93068D82B81509C0066FBC8 /* TapbackResponses.swift */; }; - D93068DA2B81509D0066FBC8 /* TapbackInputView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D93068D92B81509D0066FBC8 /* TapbackInputView.swift */; }; D93068DB2B81C85E0066FBC8 /* PowerConfig.swift in Sources */ = {isa = PBXBuildFile; fileRef = D93068DA2B81C85E0066FBC8 /* PowerConfig.swift */; }; D93068DD2B81CA820066FBC8 /* ConfigHeader.swift in Sources */ = {isa = PBXBuildFile; fileRef = D93068DC2B81CA820066FBC8 /* ConfigHeader.swift */; }; D93069082B81DF040066FBC8 /* SaveConfigButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = D93069072B81DF040066FBC8 /* SaveConfigButton.swift */; }; @@ -202,6 +201,7 @@ DD93800B2BA3F968008BEC06 /* NodeMapContent.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD93800A2BA3F968008BEC06 /* NodeMapContent.swift */; }; DD93800E2BA74D0C008BEC06 /* ChannelForm.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD93800D2BA74D0C008BEC06 /* ChannelForm.swift */; }; DD94B7402ACCE3BE00DCD1D1 /* MapSettingsForm.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD94B73F2ACCE3BE00DCD1D1 /* MapSettingsForm.swift */; }; + DD964FBD296E6B01007C176F /* EmojiOnlyTextField.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD964FBC296E6B01007C176F /* EmojiOnlyTextField.swift */; }; DD964FC2297272AE007C176F /* WaypointEntityExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD964FC1297272AE007C176F /* WaypointEntityExtension.swift */; }; DD964FC62975DBFD007C176F /* QueryCoreData.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD964FC52975DBFD007C176F /* QueryCoreData.swift */; }; DD97E96628EFD9820056DDA4 /* MeshtasticLogo.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD97E96528EFD9820056DDA4 /* MeshtasticLogo.swift */; }; @@ -215,7 +215,6 @@ DD9C70112E916EBD00106227 /* UpdateIntervalPicker.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD9C70102E916EA200106227 /* UpdateIntervalPicker.swift */; }; DDA0B6B2294CDC55001356EC /* Channels.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDA0B6B1294CDC55001356EC /* Channels.swift */; }; DDA1C48E28DB49D3009933EC /* ChannelRoles.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDA1C48D28DB49D3009933EC /* ChannelRoles.swift */; }; - DDA3DFDA2F10B39600D8F103 /* UIKeyboardType.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDA3DFD92F10B39600D8F103 /* UIKeyboardType.swift */; }; DDA6B2E928419CF2003E8C16 /* MeshPackets.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDA6B2E828419CF2003E8C16 /* MeshPackets.swift */; }; DDA9515A2BC6624100CEA535 /* TelemetryWeather.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDA951592BC6624100CEA535 /* TelemetryWeather.swift */; }; DDA9515C2BC6631200CEA535 /* TelemetryEnums.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDA9515B2BC6631200CEA535 /* TelemetryEnums.swift */; }; @@ -428,7 +427,6 @@ D93068D42B812B700066FBC8 /* MessageDestination.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageDestination.swift; sourceTree = ""; }; D93068D62B8146690066FBC8 /* MessageText.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageText.swift; sourceTree = ""; }; D93068D82B81509C0066FBC8 /* TapbackResponses.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TapbackResponses.swift; sourceTree = ""; }; - D93068D92B81509D0066FBC8 /* TapbackInputView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TapbackInputView.swift; sourceTree = ""; }; D93068DA2B81C85E0066FBC8 /* PowerConfig.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PowerConfig.swift; sourceTree = ""; }; D93068DC2B81CA820066FBC8 /* ConfigHeader.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConfigHeader.swift; sourceTree = ""; }; D93069062B81D8900066FBC8 /* MeshtasticDataModelV 27.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = "MeshtasticDataModelV 27.xcdatamodel"; sourceTree = ""; }; @@ -547,6 +545,7 @@ DD93800A2BA3F968008BEC06 /* NodeMapContent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NodeMapContent.swift; sourceTree = ""; }; DD93800D2BA74D0C008BEC06 /* ChannelForm.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChannelForm.swift; sourceTree = ""; }; DD94B73F2ACCE3BE00DCD1D1 /* MapSettingsForm.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MapSettingsForm.swift; sourceTree = ""; }; + DD964FBC296E6B01007C176F /* EmojiOnlyTextField.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EmojiOnlyTextField.swift; sourceTree = ""; }; DD964FC029724F6D007C176F /* MeshtasticDataModelV6.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = MeshtasticDataModelV6.xcdatamodel; sourceTree = ""; }; DD964FC1297272AE007C176F /* WaypointEntityExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WaypointEntityExtension.swift; sourceTree = ""; }; DD964FC52975DBFD007C176F /* QueryCoreData.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = QueryCoreData.swift; sourceTree = ""; }; @@ -564,7 +563,6 @@ DDA0B6B1294CDC55001356EC /* Channels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Channels.swift; sourceTree = ""; }; DDA1C48D28DB49D3009933EC /* ChannelRoles.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChannelRoles.swift; sourceTree = ""; }; DDA28B1B2D32C89200EF726F /* MeshtasticDataModelV 48.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = "MeshtasticDataModelV 48.xcdatamodel"; sourceTree = ""; }; - DDA3DFD92F10B39600D8F103 /* UIKeyboardType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIKeyboardType.swift; sourceTree = ""; }; DDA6B2E828419CF2003E8C16 /* MeshPackets.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MeshPackets.swift; sourceTree = ""; }; DDA951592BC6624100CEA535 /* TelemetryWeather.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TelemetryWeather.swift; sourceTree = ""; }; DDA9515B2BC6631200CEA535 /* TelemetryEnums.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TelemetryEnums.swift; sourceTree = ""; }; @@ -1252,7 +1250,6 @@ D93068D62B8146690066FBC8 /* MessageText.swift */, D93068D22B8129510066FBC8 /* MessageContextMenuItems.swift */, D93068D82B81509C0066FBC8 /* TapbackResponses.swift */, - D93068D92B81509D0066FBC8 /* TapbackInputView.swift */, ); path = Messages; sourceTree = ""; @@ -1294,6 +1291,7 @@ DD1BEF492E0292220090CE24 /* KeychainHelper.swift */, DD913638270DFF4C00D7ACF3 /* LocalNotificationManager.swift */, DDA6B2E828419CF2003E8C16 /* MeshPackets.swift */, + DD964FBC296E6B01007C176F /* EmojiOnlyTextField.swift */, DD3619142B1EF9F900C41C8C /* LocationsHandler.swift */, 6D825E612C34786C008DBEE4 /* CommonRegex.swift */, 3D3417B32E2730EC006A988B /* GeoJSONOverlayManager.swift */, @@ -1373,7 +1371,6 @@ DDDB444729F8A9C900EE2349 /* String.swift */, DD77093E2AA1B146007A8BF0 /* UIColor.swift */, DDDB444F29F8AC9C00EE2349 /* UIImage.swift */, - DDA3DFD92F10B39600D8F103 /* UIKeyboardType.swift */, DDDB443F29F79AB000EE2349 /* UserDefaults.swift */, DDB75A0E2A05920E006ED576 /* FileManager.swift */, DDB75A102A059258006ED576 /* Url.swift */, @@ -1671,6 +1668,7 @@ DD77093B2AA1ABB8007A8BF0 /* BluetoothTips.swift in Sources */, D9C9839D2B79CFD700BDBE6A /* TextMessageSize.swift in Sources */, DDC94FCE29CF55310082EA6E /* RtttlConfig.swift in Sources */, + DD964FBD296E6B01007C176F /* EmojiOnlyTextField.swift in Sources */, DD1BEF4E2E03916A0090CE24 /* ChannelsHelp.swift in Sources */, DD8169FF272476C700F4AB02 /* LogDocument.swift in Sources */, BCD93CBA2D9E11A2006C9214 /* DisconnectNodeIntent.swift in Sources */, @@ -1803,7 +1801,6 @@ DDA6B2E928419CF2003E8C16 /* MeshPackets.swift in Sources */, DDCE4E2C2869F92900BE9F8F /* UserConfig.swift in Sources */, BCB613852C68703800485544 /* NodePositionIntent.swift in Sources */, - DDA3DFDA2F10B39600D8F103 /* UIKeyboardType.swift in Sources */, DDB75A212A12B954006ED576 /* LoRaSignalStrength.swift in Sources */, DD6193752862F6E600E59241 /* ExternalNotificationConfig.swift in Sources */, DD268D8E2BCC90E2008073AE /* RouteEnums.swift in Sources */, @@ -1812,7 +1809,6 @@ DD4975A52B147BA90026544E /* AmbientLightingConfig.swift in Sources */, 3D3417D22E2DC260006A988B /* MapDataManager.swift in Sources */, D93068D92B81509C0066FBC8 /* TapbackResponses.swift in Sources */, - D93068DA2B81509D0066FBC8 /* TapbackInputView.swift in Sources */, DDA9F5E82E77FAC100E70DEB /* AnimatedNodePin.swift in Sources */, DDF82CBD2D5BC69200DC25EC /* NavigateToButton.swift in Sources */, 8D3F8A3F2D44BB02009EAAA4 /* PowerMetrics.swift in Sources */, @@ -2106,7 +2102,7 @@ "@executable_path/Frameworks", ); MACOSX_DEPLOYMENT_TARGET = 14.6; - MARKETING_VERSION = 2.7.7; + MARKETING_VERSION = 2.7.6; PRODUCT_BUNDLE_IDENTIFIER = gvh.MeshtasticClient; PRODUCT_NAME = "$(TARGET_NAME)"; SUPPORTS_MACCATALYST = YES; @@ -2141,7 +2137,7 @@ "@executable_path/Frameworks", ); MACOSX_DEPLOYMENT_TARGET = 14.6; - MARKETING_VERSION = 2.7.7; + MARKETING_VERSION = 2.7.6; PRODUCT_BUNDLE_IDENTIFIER = gvh.MeshtasticClient; PRODUCT_NAME = "$(TARGET_NAME)"; SUPPORTS_MACCATALYST = YES; @@ -2173,7 +2169,7 @@ "@executable_path/../../Frameworks", ); MACOSX_DEPLOYMENT_TARGET = 14.6; - MARKETING_VERSION = 2.7.7; + MARKETING_VERSION = 2.7.6; PRODUCT_BUNDLE_IDENTIFIER = gvh.MeshtasticClient.Widgets; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; @@ -2206,7 +2202,7 @@ "@executable_path/../../Frameworks", ); MACOSX_DEPLOYMENT_TARGET = 14.6; - MARKETING_VERSION = 2.7.7; + MARKETING_VERSION = 2.7.6; PRODUCT_BUNDLE_IDENTIFIER = gvh.MeshtasticClient.Widgets; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; diff --git a/Meshtastic/Accessory/Accessory Manager/AccessoryManager+Connect.swift b/Meshtastic/Accessory/Accessory Manager/AccessoryManager+Connect.swift index 1ec87149..d5ca0929 100644 --- a/Meshtastic/Accessory/Accessory Manager/AccessoryManager+Connect.swift +++ b/Meshtastic/Accessory/Accessory Manager/AccessoryManager+Connect.swift @@ -352,6 +352,7 @@ actor SequentialSteps { return } isRunning = false + return throw AccessoryError.tooManyRetries } diff --git a/Meshtastic/Accessory/Accessory Manager/AccessoryManager+Discovery.swift b/Meshtastic/Accessory/Accessory Manager/AccessoryManager+Discovery.swift index 1a0e9ebd..831ffe30 100644 --- a/Meshtastic/Accessory/Accessory Manager/AccessoryManager+Discovery.swift +++ b/Meshtastic/Accessory/Accessory Manager/AccessoryManager+Discovery.swift @@ -15,7 +15,7 @@ extension AccessoryManager { let tasks = transports.map { transport in Task { Logger.transport.info("🔎 [Discovery] Discovery stream started for transport \(String(describing: transport.type), privacy: .public)") - for await event in await transport.discoverDevices() { + for await event in transport.discoverDevices() { continuation.yield(event) } Logger.transport.info("🔎 [Discovery] Discovery stream closed for transport \(String(describing: transport.type), privacy: .public)") diff --git a/Meshtastic/Accessory/Accessory Manager/AccessoryManager+ToRadio.swift b/Meshtastic/Accessory/Accessory Manager/AccessoryManager+ToRadio.swift index cd1d2961..4fe2ffaf 100644 --- a/Meshtastic/Accessory/Accessory Manager/AccessoryManager+ToRadio.swift +++ b/Meshtastic/Accessory/Accessory Manager/AccessoryManager+ToRadio.swift @@ -441,6 +441,8 @@ extension AccessoryManager { Logger.services.error("Error while sending saveChannelSet request. No active device.") throw AccessoryError.ioFailed("No active device") } + var i: Int32 = 0 + var myInfo: MyInfoEntity // Before we get started delete the existing channels from the myNodeInfo if !addChannels { tryClearExistingChannels() @@ -449,74 +451,64 @@ extension AccessoryManager { let decodedString = base64UrlString.base64urlToBase64() if let decodedData = Data(base64Encoded: decodedString) { let channelSet: ChannelSet = try ChannelSet(serializedBytes: decodedData) - - var myInfo: MyInfoEntity! - var i: Int32 = 0 - - if addChannels { - let fetchMyInfoRequest = MyInfoEntity.fetchRequest() - fetchMyInfoRequest.predicate = NSPredicate(format: "myNodeNum == %lld", Int64(deviceNum)) - - let fetchedMyInfo = try context.fetch(fetchMyInfoRequest) - if fetchedMyInfo.count != 1 { - throw AccessoryError.appError("MyInfo not found") - } - - // We are trying to add a channel so lets get the last index - myInfo = fetchedMyInfo[0] - i = Int32(myInfo.channels?.count ?? -1) - - // Bail out if the index is negative or bigger than our max of 8 - if i < 0 || i > 8 { - throw AccessoryError.appError("Index out of range \(i)") - } - } - for cs in channelSet.settings { - if addChannels { - guard let mutableChannels = myInfo.channels?.mutableCopy() as? NSMutableOrderedSet else { - throw AccessoryError.appError("No channels or channel") - } - - // Bail out if there are no channels or if the same channel name already exists - if mutableChannels.first(where: { ($0 as AnyObject).name == cs.name }) is ChannelEntity { - throw AccessoryError.appError("Channel already exists") + // We are trying to add a channel so lets get the last index + let fetchMyInfoRequest = MyInfoEntity.fetchRequest() + fetchMyInfoRequest.predicate = NSPredicate(format: "myNodeNum == %lld", Int64(deviceNum)) + do { + let fetchedMyInfo = try context.fetch(fetchMyInfoRequest) + if fetchedMyInfo.count == 1 { + i = Int32(fetchedMyInfo[0].channels?.count ?? -1) + myInfo = fetchedMyInfo[0] + // Bail out if the index is negative or bigger than our max of 8 + if i < 0 || i > 8 { + throw AccessoryError.appError("Index out of range \(i)") + } + // Bail out if there are no channels or if the same channel name already exists + guard let mutableChannels = myInfo.channels!.mutableCopy() as? NSMutableOrderedSet else { + throw AccessoryError.appError("No channels or channel") + } + if mutableChannels.first(where: {($0 as AnyObject).name == cs.name }) is ChannelEntity { + throw AccessoryError.appError("Channel already exists") + } + } + } catch { + Logger.data.error("Failed to find a node MyInfo to save these channels to: \(error.localizedDescription, privacy: .public)") } } var chan = Channel() - chan.role = (i == 0) ? .primary : .secondary + if i == 0 { + chan.role = Channel.Role.primary + } else { + chan.role = Channel.Role.secondary + } chan.settings = cs chan.index = i i += 1 var adminPacket = AdminMessage() adminPacket.setChannel = chan - - var meshPacket = MeshPacket() + var meshPacket: MeshPacket = MeshPacket() meshPacket.to = UInt32(deviceNum) - meshPacket.from = UInt32(deviceNum) + meshPacket.from = UInt32(deviceNum) meshPacket.id = UInt32.random(in: UInt32(UInt8.max).. AsyncStream - func disconnect(withError: Error?, shouldReconnect: Bool) async throws + func disconnect(withError: Error?, shouldReconnect: Bool) throws func drainPendingPackets() async throws func startDrainPendingPackets() throws diff --git a/Meshtastic/Accessory/Protocols/Transport.swift b/Meshtastic/Accessory/Protocols/Transport.swift index af291869..55fa8545 100644 --- a/Meshtastic/Accessory/Protocols/Transport.swift +++ b/Meshtastic/Accessory/Protocols/Transport.swift @@ -42,10 +42,10 @@ enum DiscoveryEvent { protocol Transport { var type: TransportType { get } - var status: TransportStatus { get async } + var status: TransportStatus { get } // Discovers devices asynchronously. For ongoing scans (e.g., BLE), this can yield via AsyncStream. - func discoverDevices() async -> AsyncStream + func discoverDevices() -> AsyncStream // Connects to a device and returns a Connection. func connect(to device: Device) async throws -> any Connection diff --git a/Meshtastic/Accessory/Transports/Bluetooth Low Energy/BLEConnection.swift b/Meshtastic/Accessory/Transports/Bluetooth Low Energy/BLEConnection.swift index a1513061..f7a0f012 100644 --- a/Meshtastic/Accessory/Transports/Bluetooth Low Energy/BLEConnection.swift +++ b/Meshtastic/Accessory/Transports/Bluetooth Low Energy/BLEConnection.swift @@ -69,7 +69,7 @@ actor BLEConnection: Connection { self.delegate.setConnection(self) } - func disconnect(withError error: Error? = nil, shouldReconnect: Bool) async throws { + func disconnect(withError error: Error? = nil, shouldReconnect: Bool) throws { if peripheral.state == .connected { if let characteristic = FROMRADIO_characteristic { peripheral.setNotifyValue(false, for: characteristic) @@ -82,7 +82,7 @@ actor BLEConnection: Connection { } } - await transport?.connectionDidDisconnect(fromPeripheral: peripheral) + transport?.connectionDidDisconnect(fromPeripheral: peripheral) central.cancelPeripheralConnection(peripheral) peripheral.delegate = nil @@ -217,8 +217,8 @@ actor BLEConnection: Connection { self.connectContinuation = nil } - private func notifyTransportOfDisconnect() async { - await transport?.connectionDidDisconnect(fromPeripheral: peripheral) + private func notifyTransportOfDisconnect() { + transport?.connectionDidDisconnect(fromPeripheral: peripheral) } func startRSSITask() { @@ -450,7 +450,7 @@ actor BLEConnection: Connection { } // Inform the active connection that there was an error and it should disconnect - try await self.disconnect(withError: error, shouldReconnect: shouldReconnect) + try self.disconnect(withError: error, shouldReconnect: shouldReconnect) } func appDidEnterBackground() { diff --git a/Meshtastic/Accessory/Transports/Bluetooth Low Energy/BLETransport.swift b/Meshtastic/Accessory/Transports/Bluetooth Low Energy/BLETransport.swift index fc4953ac..aa1a32d4 100644 --- a/Meshtastic/Accessory/Transports/Bluetooth Low Energy/BLETransport.swift +++ b/Meshtastic/Accessory/Transports/Bluetooth Low Energy/BLETransport.swift @@ -10,7 +10,7 @@ import Foundation import SwiftUI import OSLog -actor BLETransport: Transport { +class BLETransport: Transport { let meshtasticServiceCBUUID = CBUUID(string: "0x6BA1B218-15A8-461F-9FA8-5DCAE273EAFD") private let kCentralRestoreID = "com.meshtastic.central" @@ -31,7 +31,7 @@ actor BLETransport: Transport { private var cleanupTask: Task? // Transport properties - let supportsManualConnection: Bool = false + var supportsManualConnection: Bool = false let requiresPeriodicHeartbeat = false init() { @@ -46,24 +46,19 @@ actor BLETransport: Transport { self.delegate.setTransport(self) } - private func setDiscoveredDeviceContinuation(_ cont: AsyncStream.Continuation?) { - self.discoveredDeviceContinuation = cont - } - - func discoverDevices() -> AsyncStream { + nonisolated func discoverDevices() -> AsyncStream { AsyncStream { cont in Task { - await self.setDiscoveredDeviceContinuation(cont) + self.discoveredDeviceContinuation = cont // This gate is opened when the CBCentralManager is in poweredOn state. // Its probably open already, but just to be sure in case we get here too quickly. try await self.setupCompleteGate.wait() - if await !self.restoreInProgress { + if !restoreInProgress { centralManager.scanForPeripherals(withServices: [meshtasticServiceCBUUID], options: [CBCentralManagerScanOptionAllowDuplicatesKey: true]) - let peripherals = await self.discoveredPeripherals.values.map({$0.peripheral}) - for alreadyDiscoveredPeripheral in peripherals { + for alreadyDiscoveredPeripheral in self.discoveredPeripherals.values.map({$0.peripheral}) { let device = Device(id: alreadyDiscoveredPeripheral.identifier, name: alreadyDiscoveredPeripheral.name ?? "Unknown", transportType: .ble, @@ -71,13 +66,11 @@ actor BLETransport: Transport { cont.yield(.deviceFound(device)) } } - await setupCleanupTask() + setupCleanupTask() } cont.onTermination = { _ in Logger.transport.error("🛜 [BLE] Discovery event stream has been canecelled.") - Task { - await self.stopScanning() - } + self.stopScanning() } } } @@ -195,12 +188,6 @@ actor BLETransport: Transport { } } - private func cancelConnectContinuation(for peripheral: CBPeripheral) { - self.connectContinuation?.resume(throwing: CancellationError()) - self.connectContinuation = nil - self.connectionDidDisconnect(fromPeripheral: peripheral) - } - func connect(to device: Device) async throws -> any Connection { guard let peripheral = discoveredPeripherals[UUID(uuidString: device.identifier)!] else { throw AccessoryError.connectionFailed("Peripheral not found") @@ -224,9 +211,9 @@ actor BLETransport: Transport { self.activeConnection = newConnection return newConnection } onCancel: { - Task { - await self.cancelConnectContinuation(for: peripheral.peripheral) - } + self.connectContinuation?.resume(throwing: CancellationError()) + self.connectContinuation = nil + self.connectionDidDisconnect(fromPeripheral: peripheral.peripheral) } Logger.transport.debug("🛜 [BLE] Connect complete.") return returnConnection @@ -239,7 +226,7 @@ actor BLETransport: Transport { Task { if await connection.peripheral.identifier == peripheral.identifier { try await connection.disconnect(withError: AccessoryError.disconnected("BLE connection lost"), shouldReconnect: true) - await self.connectionDidDisconnect(fromPeripheral: peripheral) + self.connectionDidDisconnect(fromPeripheral: peripheral) } } } @@ -277,7 +264,7 @@ actor BLETransport: Transport { Logger.transport.debug("🛜 [BLETransport] Error while connecting. Disconnecting the active connection.") Task { try? await activeConnection.disconnect(withError: error, shouldReconnect: shouldReconnect) - await self.connectionDidDisconnect(fromPeripheral: peripheral) + self.connectionDidDisconnect(fromPeripheral: peripheral) } } else { Logger.transport.error("🚨 [BLETransport] unhandled error. May be in an inconsistent state.") @@ -385,20 +372,15 @@ actor BLETransport: Transport { } Logger.transport.error("🛜 [BLE] Restoring peripheral in connecting state. ✅ didConnect Received!") - let connectTask = Task { @MainActor in - try await AccessoryManager.shared.connect(to: device, withConnection: restoredConnection, wantConfig: true, wantDatabase: true, versionCheck: true) + Task { @MainActor in + // In this case we need a full reconnect, so do the wantConfig, wantDatabase, and versionCheck + try? await AccessoryManager.shared.connect(to: device, withConnection: restoredConnection, wantConfig: true, wantDatabase: true, versionCheck: true) + restoreInProgress = false } - - do { - try await connectTask.value - } catch { - Logger.transport.error("🛜 [BLE] Error connecting during state restoration: \(error, privacy: .public)") - } - self.restoreInProgress = false } catch { - // We had a connection failure during restoration. + // We had a conneciton failure during restoration. Logger.transport.error("🛜 [BLE] Error restoring peripheral in connecting state. \(error, privacy: .public)") - self.restoreInProgress = false + restoreInProgress = false } } @@ -406,28 +388,22 @@ actor BLETransport: Transport { let restoredConnection = BLEConnection(peripheral: peripheral, central: central, transport: self) self.activeConnection = restoredConnection Logger.transport.error("🛜 [BLE] Peripheral Connection found and state is connected setting this connection as the activeConnection.") - let connectTask = Task { @MainActor in + Task { @MainActor in // In this case we need a full reconnect, so do the wantConfig, wantDatabase, and versionCheck - try await AccessoryManager.shared.connect(to: device, withConnection: restoredConnection, wantConfig: false, wantDatabase: false, versionCheck: false) + try? await AccessoryManager.shared.connect(to: device, withConnection: restoredConnection, wantConfig: false, wantDatabase: false, versionCheck: false) + restoreInProgress = false } - do { - try await connectTask.value - } catch { - Logger.transport.error("🛜 [BLE] Error connecting during state restoration: \(error, privacy: .public)") - } - - self.restoreInProgress = false Logger.transport.error("🛜 [BLE] Connection state successfully restored in the background.") default: // Since we're not going to attempt to reconnect in then allow normal device discovery Logger.transport.error("🛜 [BLE] Unhandled state restoration for state: \(cbPeripheralStateDescription(peripheral.state), privacy: .public).") - self.restoreInProgress = false + restoreInProgress = false } } } - nonisolated func device(forManualConnection: String) -> Device? { + func device(forManualConnection: String) -> Device? { return nil } @@ -462,33 +438,33 @@ class BLEDelegate: NSObject, CBCentralManagerDelegate { } func centralManagerDidUpdateState(_ central: CBCentralManager) { - Task { await transport?.handleCentralState(central.state, central: central) } + transport?.handleCentralState(central.state, central: central) } func centralManager(_ central: CBCentralManager, didDiscover peripheral: CBPeripheral, advertisementData: [String: Any], rssi RSSI: NSNumber) { - Task { await transport?.didDiscover(peripheral: peripheral, rssi: RSSI) } + transport?.didDiscover(peripheral: peripheral, rssi: RSSI) } func centralManager(_ central: CBCentralManager, didConnect peripheral: CBPeripheral) { - Task { await transport?.handleDidConnect(peripheral: peripheral, central: central) } + transport?.handleDidConnect(peripheral: peripheral, central: central) } func centralManager(_ central: CBCentralManager, didFailToConnect peripheral: CBPeripheral, error: Error?) { - Task { await transport?.handleDidFailToConnect(peripheral: peripheral, error: error) } + transport?.handleDidFailToConnect(peripheral: peripheral, error: error) } func centralManager(_ central: CBCentralManager, didDisconnectPeripheral peripheral: CBPeripheral, error: Error?) { if let error = error as? NSError { Logger.transport.error("🛜 [BLETransport] Error while disconnecting peripheral: \(peripheral.name ?? ""): \(error)") - Task { await transport?.handlePeripheralDisconnectError(peripheral: peripheral, error: error) } + transport?.handlePeripheralDisconnectError(peripheral: peripheral, error: error) } else { Logger.transport.error("🛜 [BLETransport] Did succesfully disconnect peripheral: \(peripheral.name ?? "")") - Task { await transport?.handlePeripheralDisconnect(peripheral: peripheral) } + transport?.handlePeripheralDisconnect(peripheral: peripheral) } } func centralManager(_ central: CBCentralManager, willRestoreState dict: [String: Any]) { - Task { await self.transport?.handleWillRestoreState(dict: dict, central: central) } + self.transport?.handleWillRestoreState(dict: dict, central: central) } } diff --git a/Meshtastic/Extensions/UIKeyboardType.swift b/Meshtastic/Extensions/UIKeyboardType.swift deleted file mode 100644 index 1353e35e..00000000 --- a/Meshtastic/Extensions/UIKeyboardType.swift +++ /dev/null @@ -1,13 +0,0 @@ -// -// UIKeyboard.swift -// Meshtastic -// -// Copyright(c) Garth Vander Houwen 1/7/26. -// -import UIKit - -extension UIKeyboardType { - static var emoji: UIKeyboardType { - return UIKeyboardType(rawValue: 124) ?? .default - } -} diff --git a/Meshtastic/Extensions/UserDefaults.swift b/Meshtastic/Extensions/UserDefaults.swift index 12bd86ee..82e67773 100644 --- a/Meshtastic/Extensions/UserDefaults.swift +++ b/Meshtastic/Extensions/UserDefaults.swift @@ -80,7 +80,6 @@ extension UserDefaults { case showDeviceOnboarding case usageDataAndCrashReporting case autoconnectOnDiscovery - case purgeStaleNodeDays case manualConnections case testIntEnum } @@ -179,9 +178,6 @@ extension UserDefaults { @UserDefault(.autoconnectOnDiscovery, defaultValue: true) static var autoconnectOnDiscovery: Bool - @UserDefault(.purgeStaleNodeDays, defaultValue: 0) - static var purgeStaleNodeDays: Double - @UserDefault(.testIntEnum, defaultValue: .one) static var testIntEnum: TestIntEnum diff --git a/Meshtastic/Helpers/EmojiOnlyTextField.swift b/Meshtastic/Helpers/EmojiOnlyTextField.swift index aae9e3a3..0982ab33 100644 --- a/Meshtastic/Helpers/EmojiOnlyTextField.swift +++ b/Meshtastic/Helpers/EmojiOnlyTextField.swift @@ -7,7 +7,6 @@ import SwiftUI class SwiftUIEmojiTextField: UITextField { - var shouldBecomeFirstResponderOnAppear = false func setEmoji() { _ = self.textInputMode @@ -24,39 +23,22 @@ class SwiftUIEmojiTextField: UITextField { } return nil } - - override func didMoveToWindow() { - super.didMoveToWindow() - if shouldBecomeFirstResponderOnAppear && window != nil { - DispatchQueue.main.async { [weak self] in - self?.becomeFirstResponder() - } - } - } } struct EmojiOnlyTextField: UIViewRepresentable { @Binding var text: String var placeholder: String = "" - var onBecomeFirstResponder: (() -> Void)? - var onKeyboardTypeChanged: ((Bool) -> Void)? // true if NOT emoji (should dismiss), false if emoji - var onKeyboardDismissed: (() -> Void)? // Called when keyboard is dismissed func makeUIView(context: Context) -> SwiftUIEmojiTextField { let emojiTextField = SwiftUIEmojiTextField() emojiTextField.placeholder = placeholder emojiTextField.text = text emojiTextField.delegate = context.coordinator - emojiTextField.shouldBecomeFirstResponderOnAppear = true - context.coordinator.textField = emojiTextField return emojiTextField } func updateUIView(_ uiView: SwiftUIEmojiTextField, context: Context) { uiView.text = text - context.coordinator.onBecomeFirstResponder = onBecomeFirstResponder - context.coordinator.onKeyboardTypeChanged = onKeyboardTypeChanged - context.coordinator.onKeyboardDismissed = onKeyboardDismissed } func makeCoordinator() -> Coordinator { @@ -65,41 +47,13 @@ struct EmojiOnlyTextField: UIViewRepresentable { class Coordinator: NSObject, UITextFieldDelegate { var parent: EmojiOnlyTextField - var textField: SwiftUIEmojiTextField? - var onBecomeFirstResponder: (() -> Void)? - var onKeyboardTypeChanged: ((Bool) -> Void)? - var onKeyboardDismissed: (() -> Void)? - var previousInputMode: String? - init(parent: EmojiOnlyTextField) { self.parent = parent } - - func textFieldDidBeginEditing(_ textField: UITextField) { - onBecomeFirstResponder?() - checkInputMode(textField) - } - - func textFieldDidEndEditing(_ textField: UITextField) { - // Keyboard was dismissed - onKeyboardDismissed?() - } - func textFieldDidChangeSelection(_ textField: UITextField) { DispatchQueue.main.async { [weak self] in self?.parent.text = textField.text ?? "" } - checkInputMode(textField) - } - - private func checkInputMode(_ textField: UITextField) { - if let inputMode = textField.textInputMode { - let isEmoji = inputMode.primaryLanguage == "emoji" - if previousInputMode != inputMode.primaryLanguage { - previousInputMode = inputMode.primaryLanguage - onKeyboardTypeChanged?(!isEmoji) // true if NOT emoji (should dismiss) - } - } } } } diff --git a/Meshtastic/Helpers/MeshPackets.swift b/Meshtastic/Helpers/MeshPackets.swift index 8b7a7423..255417c4 100644 --- a/Meshtastic/Helpers/MeshPackets.swift +++ b/Meshtastic/Helpers/MeshPackets.swift @@ -881,8 +881,8 @@ func telemetryPacket(packet: MeshPacket, connectedNode: Int64, context: NSManage let meshActivity = Activity.activities.first(where: { $0.attributes.nodeNum == connectedNode }) if meshActivity != nil { Task { - // await meshActivity?.update(updatedContent, alertConfiguration: alertConfiguration) - await meshActivity?.update(updatedContent) + await meshActivity?.update(updatedContent, alertConfiguration: alertConfiguration) + // await meshActivity?.update(updatedContent) Logger.services.debug("Updated live activity.") } } diff --git a/Meshtastic/Views/Messages/MessageContextMenuItems.swift b/Meshtastic/Views/Messages/MessageContextMenuItems.swift index 14d5b3f7..63104320 100644 --- a/Meshtastic/Views/Messages/MessageContextMenuItems.swift +++ b/Meshtastic/Views/Messages/MessageContextMenuItems.swift @@ -10,7 +10,6 @@ struct MessageContextMenuItems: View { let tapBackDestination: MessageDestination let isCurrentUser: Bool @Binding var isShowingDeleteConfirmation: Bool - @Binding var isShowingTapbackInput: Bool let onReply: () -> Void @State var relayDisplay: String? = nil @@ -30,8 +29,30 @@ struct MessageContextMenuItems: View { } } - Button("Tapback") { - isShowingTapbackInput = true + Menu("Tapback") { + ForEach(Tapbacks.allCases) { tb in + Button { + Task { + do { + try await accessoryManager.sendMessage( + message: tb.emojiString, + toUserNum: tapBackDestination.userNum, + channel: tapBackDestination.channelNum, + isEmoji: true, + replyID: message.messageId + ) + Task { @MainActor in + self.context.refresh(tapBackDestination.managedObject, mergeChanges: true) + } + } catch { + Logger.services.warning("Failed to send tapback.") + } + } + } label: { + Text(tb.description) + Image(uiImage: tb.emojiString.image()!) + } + } } Button(action: onReply) { diff --git a/Meshtastic/Views/Messages/MessageText.swift b/Meshtastic/Views/Messages/MessageText.swift index fe332065..28df8fba 100644 --- a/Meshtastic/Views/Messages/MessageText.swift +++ b/Meshtastic/Views/Messages/MessageText.swift @@ -27,14 +27,13 @@ struct MessageText: View { // State for handling channel URL sheet @State private var saveChannelLink: SaveChannelLinkData? @State private var isShowingDeleteConfirmation = false - @State private var isShowingTapbackInput = false - @State private var tapbackText = "" var body: some View { SessionReplayPrivacyView(textAndInputPrivacy: .maskAll) { + let markdownText = LocalizedStringKey(message.messagePayloadMarkdown ?? (message.messagePayload ?? "EMPTY MESSAGE")) - Text(markdownText) + return Text(markdownText) .tint(Self.linkBlue) .padding(.vertical, 10) .padding(.horizontal, 8) @@ -92,7 +91,6 @@ struct MessageText: View { tapBackDestination: tapBackDestination, isCurrentUser: isCurrentUser, isShowingDeleteConfirmation: $isShowingDeleteConfirmation, - isShowingTapbackInput: $isShowingTapbackInput, onReply: onReply ) } @@ -134,36 +132,6 @@ struct MessageText: View { .presentationDetents([.large]) .presentationDragIndicator(.visible) } - .sheet(isPresented: $isShowingTapbackInput) { - TapbackInputView( - text: $tapbackText, - isPresented: $isShowingTapbackInput, - onEmojiSelected: { emoji in - Task { - do { - try await accessoryManager.sendMessage( - message: emoji, - toUserNum: tapBackDestination.userNum, - channel: tapBackDestination.channelNum, - isEmoji: true, - replyID: message.messageId - ) - await MainActor.run { - switch tapBackDestination { - case let .channel(channel): - context.refresh(channel, mergeChanges: true) - case let .user(user): - context.refresh(user, mergeChanges: true) - } - } - } catch { - Logger.services.warning("Failed to send tapback.") - } - } - isShowingTapbackInput = false - } - ) - } .confirmationDialog( "Are you sure you want to delete this message?", isPresented: $isShowingDeleteConfirmation, diff --git a/Meshtastic/Views/Messages/TapbackInputView.swift b/Meshtastic/Views/Messages/TapbackInputView.swift deleted file mode 100644 index bf6de5a6..00000000 --- a/Meshtastic/Views/Messages/TapbackInputView.swift +++ /dev/null @@ -1,80 +0,0 @@ -import SwiftUI -import UIKit - -struct TapbackInputView: View { - @Binding var text: String - @Binding var isPresented: Bool - let onEmojiSelected: (String) -> Void - - var body: some View { - NavigationView { - VStack(spacing: 0) { - TextField("Tap to enter emoji", text: $text) - .keyboardType(.emoji) - .frame(height: 50) - .padding(.horizontal) - .background( - RoundedRectangle(cornerRadius: 10) - .strokeBorder(.tertiary, lineWidth: 1) - .background(RoundedRectangle(cornerRadius: 10).fill(Color(.systemBackground))) - ) - .padding(.horizontal) - .padding(.top, 8) - .onChange(of: text) { oldValue, newValue in - // Extract first emoji character and send it - if !newValue.isEmpty, let firstEmoji = extractFirstEmoji(from: newValue) { - onEmojiSelected(firstEmoji) - // Clear the text box after getting the emoji - text = "" - } - } - } - .navigationTitle("Tapback") - .navigationBarTitleDisplayMode(.inline) - .toolbar { - ToolbarItem(placement: .navigationBarTrailing) { - Button("Cancel") { - isPresented = false - } - } - } - } - .presentationDetents([.height(120)]) - } - - private func extractFirstEmoji(from string: String) -> String? { - // Extract the first emoji character(s) - handle both single and multi-scalar emojis - guard !string.isEmpty else { return nil } - - // Try to get the first character - let firstChar = string[string.startIndex] - - // Check if it's an emoji using the existing extension - if firstChar.isEmoji { - // For multi-scalar emojis (like emojis with skin tones), we need to find the full emoji sequence - var emojiEnd = string.index(after: string.startIndex) - - // Check if there are continuation scalars (for emojis with skin tones, variation selectors, etc.) - while emojiEnd < string.endIndex { - let nextChar = string[emojiEnd] - // Check if this is a continuation (variation selector, skin tone modifier, zero-width joiner, etc.) - if let scalar = nextChar.unicodeScalars.first, - (scalar.properties.isVariationSelector || - scalar.value == 0xFE0F || // Variation selector - (scalar.value >= 0x1F3FB && scalar.value <= 0x1F3FF) || // Skin tone modifiers - scalar.value == 0x200D) { // Zero-width joiner - emojiEnd = string.index(after: emojiEnd) - } else if nextChar.isEmoji { - // If it's another emoji, include it (for compound emojis like flags) - emojiEnd = string.index(after: emojiEnd) - } else { - break - } - } - - return String(string[string.startIndex.. = [.medium, .fraction(0.85)] @State private var selectedDetent: PresentationDetent = .medium @State private var waypointFailedAlert: Bool = false @@ -110,19 +111,26 @@ struct WaypointForm: View { HStack { Text("Icon") Spacer() - TextField("Select an emoji", text: $icon) - .keyboardType(.emoji) + EmojiOnlyTextField(text: $icon, placeholder: "Select an emoji") .font(.title) .focused($iconIsFocused) .onChange(of: icon) { _, value in + + // If you have anything other than emojis in your string make it empty + if !value.onlyEmojis() { + icon = "" + } // If a second emoji is entered delete the first one if value.count >= 1 { + if value.count > 1 { let index = value.index(value.startIndex, offsetBy: 1) icon = String(value[index]) } + iconIsFocused = false } } + } Toggle(isOn: $expires) { Label("Expires", systemImage: "clock.badge.xmark") @@ -450,6 +458,7 @@ struct WaypointForm: View { longitude = waypoint.coordinate.longitude } } + .presentationDetents(detents, selection: $selectedDetent) .presentationBackgroundInteraction(.enabled(upThrough: .fraction(0.85))) .presentationDragIndicator(.visible) } diff --git a/Meshtastic/Views/Nodes/MeshMap.swift b/Meshtastic/Views/Nodes/MeshMap.swift index 3e268afa..d72987da 100644 --- a/Meshtastic/Views/Nodes/MeshMap.swift +++ b/Meshtastic/Views/Nodes/MeshMap.swift @@ -120,14 +120,16 @@ struct MeshMap: View { } .sheet(item: $selectedWaypoint) { selection in WaypointForm(waypoint: selection) - .presentationDetents([.large]) + .padding() } .sheet(item: $editingWaypoint) { selection in WaypointForm(waypoint: selection, editMode: true) - .presentationDetents([.large]) + .padding() } .sheet(isPresented: $editingSettings) { MapSettingsForm(traffic: $showTraffic, pointsOfInterest: $showPointsOfInterest, mapLayer: $selectedMapLayer, meshMap: $isMeshMap, enabledOverlayConfigs: $enabledOverlayConfigs) + .presentationDetents([.large]) + } .onChange(of: router.navigationState) { guard case .map = router.navigationState.selectedTab else { return } diff --git a/Meshtastic/Views/Settings/AppSettings.swift b/Meshtastic/Views/Settings/AppSettings.swift index 243325b1..2f10c2af 100644 --- a/Meshtastic/Views/Settings/AppSettings.swift +++ b/Meshtastic/Views/Settings/AppSettings.swift @@ -120,7 +120,7 @@ struct AppSettings: View { Text("180") } } - Text("Favorited and ignored nodes are always retained. Other nodes are cleared from the app database on the schedule set by the user. (Nodes with PKC keys are always retained for at least 7 days.) This feature only purges nodes from the app that are not stored in the device node database.") + Text("Favorited and ignored nodes are always retained. Nodes without PKC keys are cleared from the app database on the schedule set by the user, nodes with PKC keys are cleared only if the interval is set to 7 days or longer. This feature only purges nodes from the app that are not stored in the device node database.") .foregroundStyle(.secondary) .font(idiom == .phone ? .caption : .callout) } diff --git a/MeshtasticProtobufs/Sources/meshtastic/admin.pb.swift b/MeshtasticProtobufs/Sources/meshtastic/admin.pb.swift index 0826bc64..fa6a9e61 100644 --- a/MeshtasticProtobufs/Sources/meshtastic/admin.pb.swift +++ b/MeshtasticProtobufs/Sources/meshtastic/admin.pb.swift @@ -21,55 +21,6 @@ fileprivate struct _GeneratedWithProtocGenSwiftVersion: SwiftProtobuf.ProtobufAP typealias Version = _2 } -/// -/// Firmware update mode for OTA updates -public enum OTAMode: SwiftProtobuf.Enum, Swift.CaseIterable { - public typealias RawValue = Int - - /// - /// Do not reboot into OTA mode - case noRebootOta // = 0 - - /// - /// Reboot into OTA mode for BLE firmware update - case otaBle // = 1 - - /// - /// Reboot into OTA mode for WiFi firmware update - case otaWifi // = 2 - case UNRECOGNIZED(Int) - - public init() { - self = .noRebootOta - } - - public init?(rawValue: Int) { - switch rawValue { - case 0: self = .noRebootOta - case 1: self = .otaBle - case 2: self = .otaWifi - default: self = .UNRECOGNIZED(rawValue) - } - } - - public var rawValue: Int { - switch self { - case .noRebootOta: return 0 - case .otaBle: return 1 - case .otaWifi: return 2 - case .UNRECOGNIZED(let i): return i - } - } - - // The compiler won't synthesize support with the UNRECOGNIZED case. - public static let allCases: [OTAMode] = [ - .noRebootOta, - .otaBle, - .otaWifi, - ] - -} - /// /// This message is handled by the Admin module and is responsible for all settings/channel read/write operations. /// This message is used to do settings operations to both remote AND local nodes. @@ -581,9 +532,6 @@ public struct AdminMessage: Sendable { /// /// Tell the node to reboot into the OTA Firmware in this many seconds (or <0 to cancel reboot) /// Only Implemented for ESP32 Devices. This needs to be issued to send a new main firmware via bluetooth. - /// Deprecated in favor of reboot_ota_mode in 2.7.17 - /// - /// NOTE: This field was marked as deprecated in the .proto file. public var rebootOtaSeconds: Int32 { get { if case .rebootOtaSeconds(let v)? = payloadVariant {return v} @@ -644,16 +592,6 @@ public struct AdminMessage: Sendable { set {payloadVariant = .nodedbReset(newValue)} } - /// - /// Tell the node to reset into the OTA Loader - public var otaRequest: AdminMessage.OTAEvent { - get { - if case .otaRequest(let v)? = payloadVariant {return v} - return AdminMessage.OTAEvent() - } - set {payloadVariant = .otaRequest(newValue)} - } - public var unknownFields = SwiftProtobuf.UnknownStorage() /// @@ -815,9 +753,6 @@ public struct AdminMessage: Sendable { /// /// Tell the node to reboot into the OTA Firmware in this many seconds (or <0 to cancel reboot) /// Only Implemented for ESP32 Devices. This needs to be issued to send a new main firmware via bluetooth. - /// Deprecated in favor of reboot_ota_mode in 2.7.17 - /// - /// NOTE: This field was marked as deprecated in the .proto file. case rebootOtaSeconds(Int32) /// /// This message is only supported for the simulator Portduino build. @@ -836,9 +771,6 @@ public struct AdminMessage: Sendable { /// Tell the node to reset the nodedb. /// When true, favorites are preserved through reset. case nodedbReset(Bool) - /// - /// Tell the node to reset into the OTA Loader - case otaRequest(AdminMessage.OTAEvent) } @@ -1127,28 +1059,6 @@ public struct AdminMessage: Sendable { public init() {} } - /// - /// User is requesting an over the air update. - /// Node will reboot into the OTA loader - public struct OTAEvent: Sendable { - // SwiftProtobuf.Message conformance is added in an extension below. See the - // `Message` and `Message+*Additions` files in the SwiftProtobuf library for - // methods supported on all messages. - - /// - /// Tell the node to reboot into OTA mode for firmware update via BLE or WiFi (ESP32 only for now) - public var rebootOtaMode: OTAMode = .noRebootOta - - /// - /// A 32 byte hash of the OTA firmware. - /// Used to verify the integrity of the firmware before applying an update. - public var otaHash: Data = Data() - - public var unknownFields = SwiftProtobuf.UnknownStorage() - - public init() {} - } - public init() {} } @@ -1329,13 +1239,9 @@ public struct KeyVerificationAdmin: Sendable { fileprivate let _protobuf_package = "meshtastic" -extension OTAMode: SwiftProtobuf._ProtoNameProviding { - public static let _protobuf_nameMap = SwiftProtobuf._NameMap(bytecode: "\0\u{2}\0NO_REBOOT_OTA\0\u{1}OTA_BLE\0\u{1}OTA_WIFI\0") -} - extension AdminMessage: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { public static let protoMessageName: String = _protobuf_package + ".AdminMessage" - public static let _protobuf_nameMap = SwiftProtobuf._NameMap(bytecode: "\0\u{3}get_channel_request\0\u{3}get_channel_response\0\u{3}get_owner_request\0\u{3}get_owner_response\0\u{3}get_config_request\0\u{3}get_config_response\0\u{3}get_module_config_request\0\u{3}get_module_config_response\0\u{4}\u{2}get_canned_message_module_messages_request\0\u{3}get_canned_message_module_messages_response\0\u{3}get_device_metadata_request\0\u{3}get_device_metadata_response\0\u{3}get_ringtone_request\0\u{3}get_ringtone_response\0\u{3}get_device_connection_status_request\0\u{3}get_device_connection_status_response\0\u{3}set_ham_mode\0\u{3}get_node_remote_hardware_pins_request\0\u{3}get_node_remote_hardware_pins_response\0\u{3}enter_dfu_mode_request\0\u{3}delete_file_request\0\u{3}set_scale\0\u{3}backup_preferences\0\u{3}restore_preferences\0\u{3}remove_backup_preferences\0\u{3}send_input_event\0\u{4}\u{5}set_owner\0\u{3}set_channel\0\u{3}set_config\0\u{3}set_module_config\0\u{3}set_canned_message_module_messages\0\u{3}set_ringtone_message\0\u{3}remove_by_nodenum\0\u{3}set_favorite_node\0\u{3}remove_favorite_node\0\u{3}set_fixed_position\0\u{3}remove_fixed_position\0\u{3}set_time_only\0\u{3}get_ui_config_request\0\u{3}get_ui_config_response\0\u{3}store_ui_config\0\u{3}set_ignored_node\0\u{3}remove_ignored_node\0\u{4}\u{10}begin_edit_settings\0\u{3}commit_edit_settings\0\u{3}add_contact\0\u{3}key_verification\0\u{4}\u{1b}factory_reset_device\0\u{3}reboot_ota_seconds\0\u{3}exit_simulator\0\u{3}reboot_seconds\0\u{3}shutdown_seconds\0\u{3}factory_reset_config\0\u{3}nodedb_reset\0\u{3}session_passkey\0\u{3}ota_request\0") + public static let _protobuf_nameMap = SwiftProtobuf._NameMap(bytecode: "\0\u{3}get_channel_request\0\u{3}get_channel_response\0\u{3}get_owner_request\0\u{3}get_owner_response\0\u{3}get_config_request\0\u{3}get_config_response\0\u{3}get_module_config_request\0\u{3}get_module_config_response\0\u{4}\u{2}get_canned_message_module_messages_request\0\u{3}get_canned_message_module_messages_response\0\u{3}get_device_metadata_request\0\u{3}get_device_metadata_response\0\u{3}get_ringtone_request\0\u{3}get_ringtone_response\0\u{3}get_device_connection_status_request\0\u{3}get_device_connection_status_response\0\u{3}set_ham_mode\0\u{3}get_node_remote_hardware_pins_request\0\u{3}get_node_remote_hardware_pins_response\0\u{3}enter_dfu_mode_request\0\u{3}delete_file_request\0\u{3}set_scale\0\u{3}backup_preferences\0\u{3}restore_preferences\0\u{3}remove_backup_preferences\0\u{3}send_input_event\0\u{4}\u{5}set_owner\0\u{3}set_channel\0\u{3}set_config\0\u{3}set_module_config\0\u{3}set_canned_message_module_messages\0\u{3}set_ringtone_message\0\u{3}remove_by_nodenum\0\u{3}set_favorite_node\0\u{3}remove_favorite_node\0\u{3}set_fixed_position\0\u{3}remove_fixed_position\0\u{3}set_time_only\0\u{3}get_ui_config_request\0\u{3}get_ui_config_response\0\u{3}store_ui_config\0\u{3}set_ignored_node\0\u{3}remove_ignored_node\0\u{4}\u{10}begin_edit_settings\0\u{3}commit_edit_settings\0\u{3}add_contact\0\u{3}key_verification\0\u{4}\u{1b}factory_reset_device\0\u{3}reboot_ota_seconds\0\u{3}exit_simulator\0\u{3}reboot_seconds\0\u{3}shutdown_seconds\0\u{3}factory_reset_config\0\u{3}nodedb_reset\0\u{3}session_passkey\0") public mutating func decodeMessage(decoder: inout D) throws { while let fieldNumber = try decoder.nextFieldNumber() { @@ -1866,19 +1772,6 @@ extension AdminMessage: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementat } }() case 101: try { try decoder.decodeSingularBytesField(value: &self.sessionPasskey) }() - case 102: try { - var v: AdminMessage.OTAEvent? - var hadOneofValue = false - if let current = self.payloadVariant { - hadOneofValue = true - if case .otaRequest(let m) = current {v = m} - } - try decoder.decodeSingularMessageField(value: &v) - if let v = v { - if hadOneofValue {try decoder.handleConflictingOneOf()} - self.payloadVariant = .otaRequest(v) - } - }() default: break } } @@ -2106,14 +1999,11 @@ extension AdminMessage: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementat guard case .nodedbReset(let v)? = self.payloadVariant else { preconditionFailure() } try visitor.visitSingularBoolField(value: v, fieldNumber: 100) }() - default: break + case nil: break } if !self.sessionPasskey.isEmpty { try visitor.visitSingularBytesField(value: self.sessionPasskey, fieldNumber: 101) } - try { if case .otaRequest(let v)? = self.payloadVariant { - try visitor.visitSingularMessageField(value: v, fieldNumber: 102) - } }() try unknownFields.traverse(visitor: &visitor) } @@ -2182,41 +2072,6 @@ extension AdminMessage.InputEvent: SwiftProtobuf.Message, SwiftProtobuf._Message } } -extension AdminMessage.OTAEvent: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { - public static let protoMessageName: String = AdminMessage.protoMessageName + ".OTAEvent" - public static let _protobuf_nameMap = SwiftProtobuf._NameMap(bytecode: "\0\u{3}reboot_ota_mode\0\u{3}ota_hash\0") - - public mutating func decodeMessage(decoder: inout D) throws { - while let fieldNumber = try decoder.nextFieldNumber() { - // The use of inline closures is to circumvent an issue where the compiler - // allocates stack space for every case branch when no optimizations are - // enabled. https://github.com/apple/swift-protobuf/issues/1034 - switch fieldNumber { - case 1: try { try decoder.decodeSingularEnumField(value: &self.rebootOtaMode) }() - case 2: try { try decoder.decodeSingularBytesField(value: &self.otaHash) }() - default: break - } - } - } - - public func traverse(visitor: inout V) throws { - if self.rebootOtaMode != .noRebootOta { - try visitor.visitSingularEnumField(value: self.rebootOtaMode, fieldNumber: 1) - } - if !self.otaHash.isEmpty { - try visitor.visitSingularBytesField(value: self.otaHash, fieldNumber: 2) - } - try unknownFields.traverse(visitor: &visitor) - } - - public static func ==(lhs: AdminMessage.OTAEvent, rhs: AdminMessage.OTAEvent) -> Bool { - if lhs.rebootOtaMode != rhs.rebootOtaMode {return false} - if lhs.otaHash != rhs.otaHash {return false} - if lhs.unknownFields != rhs.unknownFields {return false} - return true - } -} - extension HamParameters: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { public static let protoMessageName: String = _protobuf_package + ".HamParameters" public static let _protobuf_nameMap = SwiftProtobuf._NameMap(bytecode: "\0\u{3}call_sign\0\u{3}tx_power\0\u{1}frequency\0\u{3}short_name\0") diff --git a/MeshtasticProtobufs/Sources/meshtastic/mesh.pb.swift b/MeshtasticProtobufs/Sources/meshtastic/mesh.pb.swift index 99f15718..477e2457 100644 --- a/MeshtasticProtobufs/Sources/meshtastic/mesh.pb.swift +++ b/MeshtasticProtobufs/Sources/meshtastic/mesh.pb.swift @@ -166,8 +166,8 @@ public enum HardwareModel: SwiftProtobuf.Enum, Swift.CaseIterable { case loraRelayV1 // = 32 /// - /// T-Echo Plus device from LilyGo - case tEchoPlus // = 33 + /// TODO: REPLACE + case nrf52840Dk // = 33 /// /// TODO: REPLACE @@ -535,10 +535,6 @@ public enum HardwareModel: SwiftProtobuf.Enum, Swift.CaseIterable { /// Elecrow ThinkNode M6 case thinknodeM6 // = 120 - /// - /// Elecrow Meshstick 1262 - case meshstick1262 // = 121 - /// /// ------------------------------------------------------------------------------------------------------------------------------------------ /// Reserved ID For developing private Ports. These will show up in live traffic sparsely, so we can use a high number. Keep it within 8 bits. @@ -585,7 +581,7 @@ public enum HardwareModel: SwiftProtobuf.Enum, Swift.CaseIterable { case 30: self = .rp2040Lora case 31: self = .stationG2 case 32: self = .loraRelayV1 - case 33: self = .tEchoPlus + case 33: self = .nrf52840Dk case 34: self = .ppr case 35: self = .genieblocks case 36: self = .nrf52Unknown @@ -673,7 +669,6 @@ public enum HardwareModel: SwiftProtobuf.Enum, Swift.CaseIterable { case 118: self = .rak6421 case 119: self = .thinknodeM4 case 120: self = .thinknodeM6 - case 121: self = .meshstick1262 case 255: self = .privateHw default: self = .UNRECOGNIZED(rawValue) } @@ -714,7 +709,7 @@ public enum HardwareModel: SwiftProtobuf.Enum, Swift.CaseIterable { case .rp2040Lora: return 30 case .stationG2: return 31 case .loraRelayV1: return 32 - case .tEchoPlus: return 33 + case .nrf52840Dk: return 33 case .ppr: return 34 case .genieblocks: return 35 case .nrf52Unknown: return 36 @@ -802,7 +797,6 @@ public enum HardwareModel: SwiftProtobuf.Enum, Swift.CaseIterable { case .rak6421: return 118 case .thinknodeM4: return 119 case .thinknodeM6: return 120 - case .meshstick1262: return 121 case .privateHw: return 255 case .UNRECOGNIZED(let i): return i } @@ -843,7 +837,7 @@ public enum HardwareModel: SwiftProtobuf.Enum, Swift.CaseIterable { .rp2040Lora, .stationG2, .loraRelayV1, - .tEchoPlus, + .nrf52840Dk, .ppr, .genieblocks, .nrf52Unknown, @@ -931,7 +925,6 @@ public enum HardwareModel: SwiftProtobuf.Enum, Swift.CaseIterable { .rak6421, .thinknodeM4, .thinknodeM6, - .meshstick1262, .privateHw, ] @@ -1928,11 +1921,6 @@ public struct Routing: Sendable { /// Airtime fairness rate limit exceeded for a packet /// This typically enforced per portnum and is used to prevent a single node from monopolizing airtime case rateLimitExceeded // = 38 - - /// - /// PKI encryption failed, due to no public key for the remote node - /// This is different from PKI_UNKNOWN_PUBKEY which indicates a failure upon receiving a packet - case pkiSendFailPublicKey // = 39 case UNRECOGNIZED(Int) public init() { @@ -1958,7 +1946,6 @@ public struct Routing: Sendable { case 36: self = .adminBadSessionKey case 37: self = .adminPublicKeyUnauthorized case 38: self = .rateLimitExceeded - case 39: self = .pkiSendFailPublicKey default: self = .UNRECOGNIZED(rawValue) } } @@ -1982,7 +1969,6 @@ public struct Routing: Sendable { case .adminBadSessionKey: return 36 case .adminPublicKeyUnauthorized: return 37 case .rateLimitExceeded: return 38 - case .pkiSendFailPublicKey: return 39 case .UNRECOGNIZED(let i): return i } } @@ -2006,7 +1992,6 @@ public struct Routing: Sendable { .adminBadSessionKey, .adminPublicKeyUnauthorized, .rateLimitExceeded, - .pkiSendFailPublicKey, ] } @@ -2151,10 +2136,6 @@ public struct StoreForwardPlusPlus: Sendable { /// The receive time of the message in question public var encapsulatedRxtime: UInt32 = 0 - /// - /// Used in a LINK_REQUEST to specify the message X spots back from head - public var chainCount: UInt32 = 0 - public var unknownFields = SwiftProtobuf.UnknownStorage() /// @@ -3978,7 +3959,7 @@ public struct ChunkedPayloadResponse: Sendable { fileprivate let _protobuf_package = "meshtastic" extension HardwareModel: SwiftProtobuf._ProtoNameProviding { - public static let _protobuf_nameMap = SwiftProtobuf._NameMap(bytecode: "\0\u{2}\0UNSET\0\u{1}TLORA_V2\0\u{1}TLORA_V1\0\u{1}TLORA_V2_1_1P6\0\u{1}TBEAM\0\u{1}HELTEC_V2_0\0\u{1}TBEAM_V0P7\0\u{1}T_ECHO\0\u{1}TLORA_V1_1P3\0\u{1}RAK4631\0\u{1}HELTEC_V2_1\0\u{1}HELTEC_V1\0\u{1}LILYGO_TBEAM_S3_CORE\0\u{1}RAK11200\0\u{1}NANO_G1\0\u{1}TLORA_V2_1_1P8\0\u{1}TLORA_T3_S3\0\u{1}NANO_G1_EXPLORER\0\u{1}NANO_G2_ULTRA\0\u{1}LORA_TYPE\0\u{1}WIPHONE\0\u{1}WIO_WM1110\0\u{1}RAK2560\0\u{1}HELTEC_HRU_3601\0\u{1}HELTEC_WIRELESS_BRIDGE\0\u{1}STATION_G1\0\u{1}RAK11310\0\u{1}SENSELORA_RP2040\0\u{1}SENSELORA_S3\0\u{1}CANARYONE\0\u{1}RP2040_LORA\0\u{1}STATION_G2\0\u{1}LORA_RELAY_V1\0\u{1}T_ECHO_PLUS\0\u{1}PPR\0\u{1}GENIEBLOCKS\0\u{1}NRF52_UNKNOWN\0\u{1}PORTDUINO\0\u{1}ANDROID_SIM\0\u{1}DIY_V1\0\u{1}NRF52840_PCA10059\0\u{1}DR_DEV\0\u{1}M5STACK\0\u{1}HELTEC_V3\0\u{1}HELTEC_WSL_V3\0\u{1}BETAFPV_2400_TX\0\u{1}BETAFPV_900_NANO_TX\0\u{1}RPI_PICO\0\u{1}HELTEC_WIRELESS_TRACKER\0\u{1}HELTEC_WIRELESS_PAPER\0\u{1}T_DECK\0\u{1}T_WATCH_S3\0\u{1}PICOMPUTER_S3\0\u{1}HELTEC_HT62\0\u{1}EBYTE_ESP32_S3\0\u{1}ESP32_S3_PICO\0\u{1}CHATTER_2\0\u{1}HELTEC_WIRELESS_PAPER_V1_0\0\u{1}HELTEC_WIRELESS_TRACKER_V1_0\0\u{1}UNPHONE\0\u{1}TD_LORAC\0\u{1}CDEBYTE_EORA_S3\0\u{1}TWC_MESH_V4\0\u{1}NRF52_PROMICRO_DIY\0\u{1}RADIOMASTER_900_BANDIT_NANO\0\u{1}HELTEC_CAPSULE_SENSOR_V3\0\u{1}HELTEC_VISION_MASTER_T190\0\u{1}HELTEC_VISION_MASTER_E213\0\u{1}HELTEC_VISION_MASTER_E290\0\u{1}HELTEC_MESH_NODE_T114\0\u{1}SENSECAP_INDICATOR\0\u{1}TRACKER_T1000_E\0\u{1}RAK3172\0\u{1}WIO_E5\0\u{1}RADIOMASTER_900_BANDIT\0\u{1}ME25LS01_4Y10TD\0\u{1}RP2040_FEATHER_RFM95\0\u{1}M5STACK_COREBASIC\0\u{1}M5STACK_CORE2\0\u{1}RPI_PICO2\0\u{1}M5STACK_CORES3\0\u{1}SEEED_XIAO_S3\0\u{1}MS24SF1\0\u{1}TLORA_C6\0\u{1}WISMESH_TAP\0\u{1}ROUTASTIC\0\u{1}MESH_TAB\0\u{1}MESHLINK\0\u{1}XIAO_NRF52_KIT\0\u{1}THINKNODE_M1\0\u{1}THINKNODE_M2\0\u{1}T_ETH_ELITE\0\u{1}HELTEC_SENSOR_HUB\0\u{1}MUZI_BASE\0\u{1}HELTEC_MESH_POCKET\0\u{1}SEEED_SOLAR_NODE\0\u{1}NOMADSTAR_METEOR_PRO\0\u{1}CROWPANEL\0\u{1}LINK_32\0\u{1}SEEED_WIO_TRACKER_L1\0\u{1}SEEED_WIO_TRACKER_L1_EINK\0\u{1}MUZI_R1_NEO\0\u{1}T_DECK_PRO\0\u{1}T_LORA_PAGER\0\u{1}M5STACK_RESERVED\0\u{1}WISMESH_TAG\0\u{1}RAK3312\0\u{1}THINKNODE_M5\0\u{1}HELTEC_MESH_SOLAR\0\u{1}T_ECHO_LITE\0\u{1}HELTEC_V4\0\u{1}M5STACK_C6L\0\u{1}M5STACK_CARDPUTER_ADV\0\u{1}HELTEC_WIRELESS_TRACKER_V2\0\u{1}T_WATCH_ULTRA\0\u{1}THINKNODE_M3\0\u{1}WISMESH_TAP_V2\0\u{1}RAK3401\0\u{1}RAK6421\0\u{1}THINKNODE_M4\0\u{1}THINKNODE_M6\0\u{1}MESHSTICK_1262\0\u{2}F\u{2}PRIVATE_HW\0") + public static let _protobuf_nameMap = SwiftProtobuf._NameMap(bytecode: "\0\u{2}\0UNSET\0\u{1}TLORA_V2\0\u{1}TLORA_V1\0\u{1}TLORA_V2_1_1P6\0\u{1}TBEAM\0\u{1}HELTEC_V2_0\0\u{1}TBEAM_V0P7\0\u{1}T_ECHO\0\u{1}TLORA_V1_1P3\0\u{1}RAK4631\0\u{1}HELTEC_V2_1\0\u{1}HELTEC_V1\0\u{1}LILYGO_TBEAM_S3_CORE\0\u{1}RAK11200\0\u{1}NANO_G1\0\u{1}TLORA_V2_1_1P8\0\u{1}TLORA_T3_S3\0\u{1}NANO_G1_EXPLORER\0\u{1}NANO_G2_ULTRA\0\u{1}LORA_TYPE\0\u{1}WIPHONE\0\u{1}WIO_WM1110\0\u{1}RAK2560\0\u{1}HELTEC_HRU_3601\0\u{1}HELTEC_WIRELESS_BRIDGE\0\u{1}STATION_G1\0\u{1}RAK11310\0\u{1}SENSELORA_RP2040\0\u{1}SENSELORA_S3\0\u{1}CANARYONE\0\u{1}RP2040_LORA\0\u{1}STATION_G2\0\u{1}LORA_RELAY_V1\0\u{1}NRF52840DK\0\u{1}PPR\0\u{1}GENIEBLOCKS\0\u{1}NRF52_UNKNOWN\0\u{1}PORTDUINO\0\u{1}ANDROID_SIM\0\u{1}DIY_V1\0\u{1}NRF52840_PCA10059\0\u{1}DR_DEV\0\u{1}M5STACK\0\u{1}HELTEC_V3\0\u{1}HELTEC_WSL_V3\0\u{1}BETAFPV_2400_TX\0\u{1}BETAFPV_900_NANO_TX\0\u{1}RPI_PICO\0\u{1}HELTEC_WIRELESS_TRACKER\0\u{1}HELTEC_WIRELESS_PAPER\0\u{1}T_DECK\0\u{1}T_WATCH_S3\0\u{1}PICOMPUTER_S3\0\u{1}HELTEC_HT62\0\u{1}EBYTE_ESP32_S3\0\u{1}ESP32_S3_PICO\0\u{1}CHATTER_2\0\u{1}HELTEC_WIRELESS_PAPER_V1_0\0\u{1}HELTEC_WIRELESS_TRACKER_V1_0\0\u{1}UNPHONE\0\u{1}TD_LORAC\0\u{1}CDEBYTE_EORA_S3\0\u{1}TWC_MESH_V4\0\u{1}NRF52_PROMICRO_DIY\0\u{1}RADIOMASTER_900_BANDIT_NANO\0\u{1}HELTEC_CAPSULE_SENSOR_V3\0\u{1}HELTEC_VISION_MASTER_T190\0\u{1}HELTEC_VISION_MASTER_E213\0\u{1}HELTEC_VISION_MASTER_E290\0\u{1}HELTEC_MESH_NODE_T114\0\u{1}SENSECAP_INDICATOR\0\u{1}TRACKER_T1000_E\0\u{1}RAK3172\0\u{1}WIO_E5\0\u{1}RADIOMASTER_900_BANDIT\0\u{1}ME25LS01_4Y10TD\0\u{1}RP2040_FEATHER_RFM95\0\u{1}M5STACK_COREBASIC\0\u{1}M5STACK_CORE2\0\u{1}RPI_PICO2\0\u{1}M5STACK_CORES3\0\u{1}SEEED_XIAO_S3\0\u{1}MS24SF1\0\u{1}TLORA_C6\0\u{1}WISMESH_TAP\0\u{1}ROUTASTIC\0\u{1}MESH_TAB\0\u{1}MESHLINK\0\u{1}XIAO_NRF52_KIT\0\u{1}THINKNODE_M1\0\u{1}THINKNODE_M2\0\u{1}T_ETH_ELITE\0\u{1}HELTEC_SENSOR_HUB\0\u{1}MUZI_BASE\0\u{1}HELTEC_MESH_POCKET\0\u{1}SEEED_SOLAR_NODE\0\u{1}NOMADSTAR_METEOR_PRO\0\u{1}CROWPANEL\0\u{1}LINK_32\0\u{1}SEEED_WIO_TRACKER_L1\0\u{1}SEEED_WIO_TRACKER_L1_EINK\0\u{1}MUZI_R1_NEO\0\u{1}T_DECK_PRO\0\u{1}T_LORA_PAGER\0\u{1}M5STACK_RESERVED\0\u{1}WISMESH_TAG\0\u{1}RAK3312\0\u{1}THINKNODE_M5\0\u{1}HELTEC_MESH_SOLAR\0\u{1}T_ECHO_LITE\0\u{1}HELTEC_V4\0\u{1}M5STACK_C6L\0\u{1}M5STACK_CARDPUTER_ADV\0\u{1}HELTEC_WIRELESS_TRACKER_V2\0\u{1}T_WATCH_ULTRA\0\u{1}THINKNODE_M3\0\u{1}WISMESH_TAP_V2\0\u{1}RAK3401\0\u{1}RAK6421\0\u{1}THINKNODE_M4\0\u{1}THINKNODE_M6\0\u{2}G\u{2}PRIVATE_HW\0") } extension Constants: SwiftProtobuf._ProtoNameProviding { @@ -4428,7 +4409,7 @@ extension Routing: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBa } extension Routing.Error: SwiftProtobuf._ProtoNameProviding { - public static let _protobuf_nameMap = SwiftProtobuf._NameMap(bytecode: "\0\u{2}\0NONE\0\u{1}NO_ROUTE\0\u{1}GOT_NAK\0\u{1}TIMEOUT\0\u{1}NO_INTERFACE\0\u{1}MAX_RETRANSMIT\0\u{1}NO_CHANNEL\0\u{1}TOO_LARGE\0\u{1}NO_RESPONSE\0\u{1}DUTY_CYCLE_LIMIT\0\u{2}\u{17}BAD_REQUEST\0\u{1}NOT_AUTHORIZED\0\u{1}PKI_FAILED\0\u{1}PKI_UNKNOWN_PUBKEY\0\u{1}ADMIN_BAD_SESSION_KEY\0\u{1}ADMIN_PUBLIC_KEY_UNAUTHORIZED\0\u{1}RATE_LIMIT_EXCEEDED\0\u{1}PKI_SEND_FAIL_PUBLIC_KEY\0") + public static let _protobuf_nameMap = SwiftProtobuf._NameMap(bytecode: "\0\u{2}\0NONE\0\u{1}NO_ROUTE\0\u{1}GOT_NAK\0\u{1}TIMEOUT\0\u{1}NO_INTERFACE\0\u{1}MAX_RETRANSMIT\0\u{1}NO_CHANNEL\0\u{1}TOO_LARGE\0\u{1}NO_RESPONSE\0\u{1}DUTY_CYCLE_LIMIT\0\u{2}\u{17}BAD_REQUEST\0\u{1}NOT_AUTHORIZED\0\u{1}PKI_FAILED\0\u{1}PKI_UNKNOWN_PUBKEY\0\u{1}ADMIN_BAD_SESSION_KEY\0\u{1}ADMIN_PUBLIC_KEY_UNAUTHORIZED\0\u{1}RATE_LIMIT_EXCEEDED\0") } extension DataMessage: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { @@ -4547,7 +4528,7 @@ extension KeyVerification: SwiftProtobuf.Message, SwiftProtobuf._MessageImplemen extension StoreForwardPlusPlus: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { public static let protoMessageName: String = _protobuf_package + ".StoreForwardPlusPlus" - public static let _protobuf_nameMap = SwiftProtobuf._NameMap(bytecode: "\0\u{3}sfpp_message_type\0\u{3}message_hash\0\u{3}commit_hash\0\u{3}root_hash\0\u{1}message\0\u{3}encapsulated_id\0\u{3}encapsulated_to\0\u{3}encapsulated_from\0\u{3}encapsulated_rxtime\0\u{3}chain_count\0") + public static let _protobuf_nameMap = SwiftProtobuf._NameMap(bytecode: "\0\u{3}sfpp_message_type\0\u{3}message_hash\0\u{3}commit_hash\0\u{3}root_hash\0\u{1}message\0\u{3}encapsulated_id\0\u{3}encapsulated_to\0\u{3}encapsulated_from\0\u{3}encapsulated_rxtime\0") public mutating func decodeMessage(decoder: inout D) throws { while let fieldNumber = try decoder.nextFieldNumber() { @@ -4564,7 +4545,6 @@ extension StoreForwardPlusPlus: SwiftProtobuf.Message, SwiftProtobuf._MessageImp case 7: try { try decoder.decodeSingularUInt32Field(value: &self.encapsulatedTo) }() case 8: try { try decoder.decodeSingularUInt32Field(value: &self.encapsulatedFrom) }() case 9: try { try decoder.decodeSingularUInt32Field(value: &self.encapsulatedRxtime) }() - case 10: try { try decoder.decodeSingularUInt32Field(value: &self.chainCount) }() default: break } } @@ -4598,9 +4578,6 @@ extension StoreForwardPlusPlus: SwiftProtobuf.Message, SwiftProtobuf._MessageImp if self.encapsulatedRxtime != 0 { try visitor.visitSingularUInt32Field(value: self.encapsulatedRxtime, fieldNumber: 9) } - if self.chainCount != 0 { - try visitor.visitSingularUInt32Field(value: self.chainCount, fieldNumber: 10) - } try unknownFields.traverse(visitor: &visitor) } @@ -4614,7 +4591,6 @@ extension StoreForwardPlusPlus: SwiftProtobuf.Message, SwiftProtobuf._MessageImp if lhs.encapsulatedTo != rhs.encapsulatedTo {return false} if lhs.encapsulatedFrom != rhs.encapsulatedFrom {return false} if lhs.encapsulatedRxtime != rhs.encapsulatedRxtime {return false} - if lhs.chainCount != rhs.chainCount {return false} if lhs.unknownFields != rhs.unknownFields {return false} return true } diff --git a/protobufs b/protobufs index 4ed2d1a3..62ef17b3 160000 --- a/protobufs +++ b/protobufs @@ -1 +1 @@ -Subproject commit 4ed2d1a35e7f486708ead6d06fb2597c9aa87245 +Subproject commit 62ef17b3d1625fc6d78ed661f614d0baad4be9ef From 3eef38926f1fb31a19c5ef428ee2491777a6450c Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Thu, 15 Jan 2026 14:13:40 -0800 Subject: [PATCH 3/4] 2.7.7 Working Changes (#1551) * Bump version * update the translations (#1540) update the translations * Don't alert (with sound: .default) when updating Live Activity (#1536) * Fix adding channels (#1532) * Full translation into Spanish (#1529) * tapback with any emoji (#1538) * Call clearStaleNodes at start of sendWantConfig (#1535) * NFC Tag contact (#1537) * Accessorymanager background discovery (#1542) * Don't add new BLE devices to the device list in the backgournd * Bump version * Update Meshtastic/MeshtasticApp.swift Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Update Meshtastic/MeshtasticApp.swift Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --------- Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Revert "Full translation into Spanish (#1529)" (#1543) This reverts commit f25fdfb89fba70d22cf1d281c62f956f94d6343c. * Revert "update the translations (#1540)" (#1544) This reverts commit cb2fd8cc15185f6b9ce8a940d8ca8d11a32a2f80. * Revert "NFC Tag contact (#1537)" (#1545) This reverts commit 5c22b8b6e0176f4927bfc79234dabe109b215edf. * Update Meshtastic/Views/Messages/TapbackInputView.swift Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Update Meshtastic/Helpers/EmojiOnlyTextField.swift Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Revert "Accessorymanager background discovery (#1542)" (#1553) This reverts commit 487f24b99a4f3d0b4491ee7a2c86dcffb7f62c7f. * Update protobufs * Remove UI Kit code, clean up waypoint form emoji picker * Remove redundant nested Task in tapback emoji handler (#1552) * Initial plan * Remove nested Task block in tapback handler Co-authored-by: garthvh <1795163+garthvh@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> * Delete empty file * Handle nil for emoji keyboard type extension * Remove UI kit method from waypoint form emoji picker * Remove UI kit emoji picker from tapback * Add Exchange User Info (#1550) * Emoji keyboard (#1559) * Add file missing from project, must have merged badly * Remove ui kit emoji keyboard * Discovery background fixes (#1561) * Make BLE Transport an actor to fix background discovery crashes * Protobufs * Update Meshtastic/Accessory/Transports/Bluetooth Low Energy/BLETransport.swift Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Throw too many retries error again, remove return --------- Co-authored-by: Ben Meadors Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Increase connection timeout * Update protobufs * Revert "Fix adding channels (#1532)" (#1562) This reverts commit bff8ca018ba1b673ceb91ecd80b706b25c707b88. --------- Co-authored-by: MGJ <62177301+MGJ520@users.noreply.github.com> Co-authored-by: Mike Robbins Co-authored-by: Benjamin Faershtein <119711889+RCGV1@users.noreply.github.com> Co-authored-by: Alvaro Samudio Co-authored-by: Mathew Kamkar <578302+matkam@users.noreply.github.com> Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> Co-authored-by: Ben Meadors Co-authored-by: Copilot <198982749+Copilot@users.noreply.github.com> Co-authored-by: garthvh <1795163+garthvh@users.noreply.github.com> Co-authored-by: Brian Hardie <777730+bhardie@users.noreply.github.com> --- Localizable.xcstrings | 27 +- Meshtastic.xcodeproj/project.pbxproj | 24 +- .../AccessoryManager+Connect.swift | 3 +- .../AccessoryManager+Discovery.swift | 2 +- .../AccessoryManager+ToRadio.swift | 30 +++ .../Accessory Manager/AccessoryManager.swift | 2 + .../Accessory/Protocols/Connection.swift | 2 +- .../Accessory/Protocols/Transport.swift | 4 +- .../Bluetooth Low Energy/BLEConnection.swift | 10 +- .../Bluetooth Low Energy/BLETransport.swift | 86 +++--- Meshtastic/Extensions/UIKeyboardType.swift | 13 + Meshtastic/Extensions/UserDefaults.swift | 4 + Meshtastic/Helpers/EmojiOnlyTextField.swift | 46 ++++ Meshtastic/Helpers/MeshPackets.swift | 4 +- .../Messages/MessageContextMenuItems.swift | 28 +- Meshtastic/Views/Messages/MessageText.swift | 246 +++++++++++------- .../Views/Messages/TapbackInputView.swift | 80 ++++++ .../Actions/ExchangeUserInfoButton.swift | 61 +++++ .../Nodes/Helpers/Map/WaypointForm.swift | 13 +- .../Views/Nodes/Helpers/NodeDetail.swift | 4 + Meshtastic/Views/Nodes/MeshMap.swift | 6 +- Meshtastic/Views/Nodes/NodeList.swift | 13 + Meshtastic/Views/Settings/AppSettings.swift | 2 +- .../Sources/meshtastic/admin.pb.swift | 174 ++++++++++++- .../Sources/meshtastic/config.pb.swift | 2 +- .../Sources/meshtastic/deviceonly.pb.swift | 1 + .../Sources/meshtastic/mesh.pb.swift | 71 ++++- .../Sources/meshtastic/module_config.pb.swift | 29 ++- protobufs | 2 +- 29 files changed, 783 insertions(+), 206 deletions(-) create mode 100644 Meshtastic/Extensions/UIKeyboardType.swift create mode 100644 Meshtastic/Views/Messages/TapbackInputView.swift create mode 100644 Meshtastic/Views/Nodes/Helpers/Actions/ExchangeUserInfoButton.swift diff --git a/Localizable.xcstrings b/Localizable.xcstrings index 33be032d..3dc62e68 100644 --- a/Localizable.xcstrings +++ b/Localizable.xcstrings @@ -13709,6 +13709,9 @@ } } } + }, + "Exchange User Info" : { + }, "Exclamation" : { "localizations" : { @@ -14189,6 +14192,9 @@ } } } + }, + "Failed to exchange user info." : { + }, "Failed to get a valid position to exchange" : { "localizations" : { @@ -14315,6 +14321,7 @@ } }, "Favorited and ignored nodes are always retained. Nodes without PKC keys are cleared from the app database on the schedule set by the user, nodes with PKC keys are cleared only if the interval is set to 7 days or longer. This feature only purges nodes from the app that are not stored in the device node database." : { + "extractionState" : "stale", "localizations" : { "de" : { "stringUnit" : { @@ -14335,6 +14342,9 @@ } } } + }, + "Favorited and ignored nodes are always retained. Other nodes are cleared from the app database on the schedule set by the user. (Nodes with PKC keys are always retained for at least 7 days.) This feature only purges nodes from the app that are not stored in the device node database." : { + }, "Favorites" : { "localizations" : { @@ -31617,6 +31627,9 @@ } } } + }, + "Select an emoji" : { + }, "Select Channel" : { "localizations" : { @@ -35547,6 +35560,9 @@ } } } + }, + "Tap to enter emoji" : { + }, "Tapback" : { "localizations" : { @@ -40227,6 +40243,12 @@ } } } + }, + "User Info Exchange Failed" : { + + }, + "User Info Sent" : { + }, "User Privacy" : { @@ -42318,7 +42340,10 @@ } } } + }, + "Your user info has been sent with a request for a response with their user info." : { + } }, "version" : "1.1" -} +} \ No newline at end of file diff --git a/Meshtastic.xcodeproj/project.pbxproj b/Meshtastic.xcodeproj/project.pbxproj index b87ce5f2..817570e2 100644 --- a/Meshtastic.xcodeproj/project.pbxproj +++ b/Meshtastic.xcodeproj/project.pbxproj @@ -115,6 +115,7 @@ D93068D52B812B700066FBC8 /* MessageDestination.swift in Sources */ = {isa = PBXBuildFile; fileRef = D93068D42B812B700066FBC8 /* MessageDestination.swift */; }; D93068D72B8146690066FBC8 /* MessageText.swift in Sources */ = {isa = PBXBuildFile; fileRef = D93068D62B8146690066FBC8 /* MessageText.swift */; }; D93068D92B81509C0066FBC8 /* TapbackResponses.swift in Sources */ = {isa = PBXBuildFile; fileRef = D93068D82B81509C0066FBC8 /* TapbackResponses.swift */; }; + D93068DA2B81509D0066FBC8 /* TapbackInputView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D93068D92B81509D0066FBC8 /* TapbackInputView.swift */; }; D93068DB2B81C85E0066FBC8 /* PowerConfig.swift in Sources */ = {isa = PBXBuildFile; fileRef = D93068DA2B81C85E0066FBC8 /* PowerConfig.swift */; }; D93068DD2B81CA820066FBC8 /* ConfigHeader.swift in Sources */ = {isa = PBXBuildFile; fileRef = D93068DC2B81CA820066FBC8 /* ConfigHeader.swift */; }; D93069082B81DF040066FBC8 /* SaveConfigButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = D93069072B81DF040066FBC8 /* SaveConfigButton.swift */; }; @@ -159,6 +160,7 @@ DD3CC6C028E7A60700FA9159 /* MessagingEnums.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD3CC6BF28E7A60700FA9159 /* MessagingEnums.swift */; }; DD3CC6C228EB9D4900FA9159 /* UpdateCoreData.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD3CC6C128EB9D4900FA9159 /* UpdateCoreData.swift */; }; DD3D17E02C3FB67200561584 /* LocalWeatherConditions.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD3D17DF2C3FB67200561584 /* LocalWeatherConditions.swift */; }; + DD4074692F1233F400BCC22F /* ExchangeUserInfoButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD4074682F1233F400BCC22F /* ExchangeUserInfoButton.swift */; }; DD41582628582E9B009B0E59 /* DeviceConfig.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD41582528582E9B009B0E59 /* DeviceConfig.swift */; }; DD415828285859C4009B0E59 /* TelemetryConfig.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD415827285859C4009B0E59 /* TelemetryConfig.swift */; }; DD41582A28585C32009B0E59 /* RangeTestConfig.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD41582928585C32009B0E59 /* RangeTestConfig.swift */; }; @@ -201,7 +203,6 @@ DD93800B2BA3F968008BEC06 /* NodeMapContent.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD93800A2BA3F968008BEC06 /* NodeMapContent.swift */; }; DD93800E2BA74D0C008BEC06 /* ChannelForm.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD93800D2BA74D0C008BEC06 /* ChannelForm.swift */; }; DD94B7402ACCE3BE00DCD1D1 /* MapSettingsForm.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD94B73F2ACCE3BE00DCD1D1 /* MapSettingsForm.swift */; }; - DD964FBD296E6B01007C176F /* EmojiOnlyTextField.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD964FBC296E6B01007C176F /* EmojiOnlyTextField.swift */; }; DD964FC2297272AE007C176F /* WaypointEntityExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD964FC1297272AE007C176F /* WaypointEntityExtension.swift */; }; DD964FC62975DBFD007C176F /* QueryCoreData.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD964FC52975DBFD007C176F /* QueryCoreData.swift */; }; DD97E96628EFD9820056DDA4 /* MeshtasticLogo.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD97E96528EFD9820056DDA4 /* MeshtasticLogo.swift */; }; @@ -215,6 +216,7 @@ DD9C70112E916EBD00106227 /* UpdateIntervalPicker.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD9C70102E916EA200106227 /* UpdateIntervalPicker.swift */; }; DDA0B6B2294CDC55001356EC /* Channels.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDA0B6B1294CDC55001356EC /* Channels.swift */; }; DDA1C48E28DB49D3009933EC /* ChannelRoles.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDA1C48D28DB49D3009933EC /* ChannelRoles.swift */; }; + DDA3DFDA2F10B39600D8F103 /* UIKeyboardType.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDA3DFD92F10B39600D8F103 /* UIKeyboardType.swift */; }; DDA6B2E928419CF2003E8C16 /* MeshPackets.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDA6B2E828419CF2003E8C16 /* MeshPackets.swift */; }; DDA9515A2BC6624100CEA535 /* TelemetryWeather.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDA951592BC6624100CEA535 /* TelemetryWeather.swift */; }; DDA9515C2BC6631200CEA535 /* TelemetryEnums.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDA9515B2BC6631200CEA535 /* TelemetryEnums.swift */; }; @@ -427,6 +429,7 @@ D93068D42B812B700066FBC8 /* MessageDestination.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageDestination.swift; sourceTree = ""; }; D93068D62B8146690066FBC8 /* MessageText.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageText.swift; sourceTree = ""; }; D93068D82B81509C0066FBC8 /* TapbackResponses.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TapbackResponses.swift; sourceTree = ""; }; + D93068D92B81509D0066FBC8 /* TapbackInputView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TapbackInputView.swift; sourceTree = ""; }; D93068DA2B81C85E0066FBC8 /* PowerConfig.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PowerConfig.swift; sourceTree = ""; }; D93068DC2B81CA820066FBC8 /* ConfigHeader.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConfigHeader.swift; sourceTree = ""; }; D93069062B81D8900066FBC8 /* MeshtasticDataModelV 27.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = "MeshtasticDataModelV 27.xcdatamodel"; sourceTree = ""; }; @@ -490,6 +493,7 @@ DD3CC6C128EB9D4900FA9159 /* UpdateCoreData.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UpdateCoreData.swift; sourceTree = ""; }; DD3D17DC2C3D7B1400561584 /* MeshtasticDataModelV 39.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = "MeshtasticDataModelV 39.xcdatamodel"; sourceTree = ""; }; DD3D17DF2C3FB67200561584 /* LocalWeatherConditions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocalWeatherConditions.swift; sourceTree = ""; }; + DD4074682F1233F400BCC22F /* ExchangeUserInfoButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExchangeUserInfoButton.swift; sourceTree = ""; }; DD41582528582E9B009B0E59 /* DeviceConfig.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeviceConfig.swift; sourceTree = ""; }; DD415827285859C4009B0E59 /* TelemetryConfig.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TelemetryConfig.swift; sourceTree = ""; }; DD41582928585C32009B0E59 /* RangeTestConfig.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RangeTestConfig.swift; sourceTree = ""; }; @@ -545,7 +549,6 @@ DD93800A2BA3F968008BEC06 /* NodeMapContent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NodeMapContent.swift; sourceTree = ""; }; DD93800D2BA74D0C008BEC06 /* ChannelForm.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChannelForm.swift; sourceTree = ""; }; DD94B73F2ACCE3BE00DCD1D1 /* MapSettingsForm.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MapSettingsForm.swift; sourceTree = ""; }; - DD964FBC296E6B01007C176F /* EmojiOnlyTextField.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EmojiOnlyTextField.swift; sourceTree = ""; }; DD964FC029724F6D007C176F /* MeshtasticDataModelV6.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = MeshtasticDataModelV6.xcdatamodel; sourceTree = ""; }; DD964FC1297272AE007C176F /* WaypointEntityExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WaypointEntityExtension.swift; sourceTree = ""; }; DD964FC52975DBFD007C176F /* QueryCoreData.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = QueryCoreData.swift; sourceTree = ""; }; @@ -563,6 +566,7 @@ DDA0B6B1294CDC55001356EC /* Channels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Channels.swift; sourceTree = ""; }; DDA1C48D28DB49D3009933EC /* ChannelRoles.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChannelRoles.swift; sourceTree = ""; }; DDA28B1B2D32C89200EF726F /* MeshtasticDataModelV 48.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = "MeshtasticDataModelV 48.xcdatamodel"; sourceTree = ""; }; + DDA3DFD92F10B39600D8F103 /* UIKeyboardType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIKeyboardType.swift; sourceTree = ""; }; DDA6B2E828419CF2003E8C16 /* MeshPackets.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MeshPackets.swift; sourceTree = ""; }; DDA951592BC6624100CEA535 /* TelemetryWeather.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TelemetryWeather.swift; sourceTree = ""; }; DDA9515B2BC6631200CEA535 /* TelemetryEnums.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TelemetryEnums.swift; sourceTree = ""; }; @@ -844,6 +848,7 @@ 251926882C3BAF2E00249DF5 /* Actions */ = { isa = PBXGroup; children = ( + DD4074682F1233F400BCC22F /* ExchangeUserInfoButton.swift */, DDDFE73E2D0D48FF0044463C /* IgnoreNodeButton.swift */, 251926842C3BA97800249DF5 /* FavoriteNodeButton.swift */, 251926892C3BB1B200249DF5 /* ExchangePositionsButton.swift */, @@ -1250,6 +1255,7 @@ D93068D62B8146690066FBC8 /* MessageText.swift */, D93068D22B8129510066FBC8 /* MessageContextMenuItems.swift */, D93068D82B81509C0066FBC8 /* TapbackResponses.swift */, + D93068D92B81509D0066FBC8 /* TapbackInputView.swift */, ); path = Messages; sourceTree = ""; @@ -1291,7 +1297,6 @@ DD1BEF492E0292220090CE24 /* KeychainHelper.swift */, DD913638270DFF4C00D7ACF3 /* LocalNotificationManager.swift */, DDA6B2E828419CF2003E8C16 /* MeshPackets.swift */, - DD964FBC296E6B01007C176F /* EmojiOnlyTextField.swift */, DD3619142B1EF9F900C41C8C /* LocationsHandler.swift */, 6D825E612C34786C008DBEE4 /* CommonRegex.swift */, 3D3417B32E2730EC006A988B /* GeoJSONOverlayManager.swift */, @@ -1371,6 +1376,7 @@ DDDB444729F8A9C900EE2349 /* String.swift */, DD77093E2AA1B146007A8BF0 /* UIColor.swift */, DDDB444F29F8AC9C00EE2349 /* UIImage.swift */, + DDA3DFD92F10B39600D8F103 /* UIKeyboardType.swift */, DDDB443F29F79AB000EE2349 /* UserDefaults.swift */, DDB75A0E2A05920E006ED576 /* FileManager.swift */, DDB75A102A059258006ED576 /* Url.swift */, @@ -1668,7 +1674,6 @@ DD77093B2AA1ABB8007A8BF0 /* BluetoothTips.swift in Sources */, D9C9839D2B79CFD700BDBE6A /* TextMessageSize.swift in Sources */, DDC94FCE29CF55310082EA6E /* RtttlConfig.swift in Sources */, - DD964FBD296E6B01007C176F /* EmojiOnlyTextField.swift in Sources */, DD1BEF4E2E03916A0090CE24 /* ChannelsHelp.swift in Sources */, DD8169FF272476C700F4AB02 /* LogDocument.swift in Sources */, BCD93CBA2D9E11A2006C9214 /* DisconnectNodeIntent.swift in Sources */, @@ -1721,6 +1726,7 @@ DDDB444A29F8AA3A00EE2349 /* CLLocationCoordinate2D.swift in Sources */, 25C49D902C471AEA0024FBD1 /* Constants.swift in Sources */, ABB99DEB2E2EA1C500CFBD05 /* AppIconPicker.swift in Sources */, + DD4074692F1233F400BCC22F /* ExchangeUserInfoButton.swift in Sources */, DD41582628582E9B009B0E59 /* DeviceConfig.swift in Sources */, DDF45C372BC46A5A005ED5F2 /* TimeZone.swift in Sources */, DD007BAE2AA4E91200F5FA12 /* MyInfoEntityExtension.swift in Sources */, @@ -1801,6 +1807,7 @@ DDA6B2E928419CF2003E8C16 /* MeshPackets.swift in Sources */, DDCE4E2C2869F92900BE9F8F /* UserConfig.swift in Sources */, BCB613852C68703800485544 /* NodePositionIntent.swift in Sources */, + DDA3DFDA2F10B39600D8F103 /* UIKeyboardType.swift in Sources */, DDB75A212A12B954006ED576 /* LoRaSignalStrength.swift in Sources */, DD6193752862F6E600E59241 /* ExternalNotificationConfig.swift in Sources */, DD268D8E2BCC90E2008073AE /* RouteEnums.swift in Sources */, @@ -1809,6 +1816,7 @@ DD4975A52B147BA90026544E /* AmbientLightingConfig.swift in Sources */, 3D3417D22E2DC260006A988B /* MapDataManager.swift in Sources */, D93068D92B81509C0066FBC8 /* TapbackResponses.swift in Sources */, + D93068DA2B81509D0066FBC8 /* TapbackInputView.swift in Sources */, DDA9F5E82E77FAC100E70DEB /* AnimatedNodePin.swift in Sources */, DDF82CBD2D5BC69200DC25EC /* NavigateToButton.swift in Sources */, 8D3F8A3F2D44BB02009EAAA4 /* PowerMetrics.swift in Sources */, @@ -2102,7 +2110,7 @@ "@executable_path/Frameworks", ); MACOSX_DEPLOYMENT_TARGET = 14.6; - MARKETING_VERSION = 2.7.6; + MARKETING_VERSION = 2.7.7; PRODUCT_BUNDLE_IDENTIFIER = gvh.MeshtasticClient; PRODUCT_NAME = "$(TARGET_NAME)"; SUPPORTS_MACCATALYST = YES; @@ -2137,7 +2145,7 @@ "@executable_path/Frameworks", ); MACOSX_DEPLOYMENT_TARGET = 14.6; - MARKETING_VERSION = 2.7.6; + MARKETING_VERSION = 2.7.7; PRODUCT_BUNDLE_IDENTIFIER = gvh.MeshtasticClient; PRODUCT_NAME = "$(TARGET_NAME)"; SUPPORTS_MACCATALYST = YES; @@ -2169,7 +2177,7 @@ "@executable_path/../../Frameworks", ); MACOSX_DEPLOYMENT_TARGET = 14.6; - MARKETING_VERSION = 2.7.6; + MARKETING_VERSION = 2.7.7; PRODUCT_BUNDLE_IDENTIFIER = gvh.MeshtasticClient.Widgets; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; @@ -2202,7 +2210,7 @@ "@executable_path/../../Frameworks", ); MACOSX_DEPLOYMENT_TARGET = 14.6; - MARKETING_VERSION = 2.7.6; + MARKETING_VERSION = 2.7.7; PRODUCT_BUNDLE_IDENTIFIER = gvh.MeshtasticClient.Widgets; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; diff --git a/Meshtastic/Accessory/Accessory Manager/AccessoryManager+Connect.swift b/Meshtastic/Accessory/Accessory Manager/AccessoryManager+Connect.swift index d5ca0929..456001d8 100644 --- a/Meshtastic/Accessory/Accessory Manager/AccessoryManager+Connect.swift +++ b/Meshtastic/Accessory/Accessory Manager/AccessoryManager+Connect.swift @@ -48,7 +48,7 @@ extension AccessoryManager { } // Step 1: Setup the connection - Step(timeout: .seconds(2)) { @MainActor _ in + Step(timeout: .seconds(5)) { @MainActor _ in Logger.transport.info("🔗👟[Connect] Step 1: connection to \(device.id, privacy: .public)") do { let connection: Connection @@ -352,7 +352,6 @@ actor SequentialSteps { return } isRunning = false - return throw AccessoryError.tooManyRetries } diff --git a/Meshtastic/Accessory/Accessory Manager/AccessoryManager+Discovery.swift b/Meshtastic/Accessory/Accessory Manager/AccessoryManager+Discovery.swift index 831ffe30..1a0e9ebd 100644 --- a/Meshtastic/Accessory/Accessory Manager/AccessoryManager+Discovery.swift +++ b/Meshtastic/Accessory/Accessory Manager/AccessoryManager+Discovery.swift @@ -15,7 +15,7 @@ extension AccessoryManager { let tasks = transports.map { transport in Task { Logger.transport.info("🔎 [Discovery] Discovery stream started for transport \(String(describing: transport.type), privacy: .public)") - for await event in transport.discoverDevices() { + for await event in await transport.discoverDevices() { continuation.yield(event) } Logger.transport.info("🔎 [Discovery] Discovery stream closed for transport \(String(describing: transport.type), privacy: .public)") diff --git a/Meshtastic/Accessory/Accessory Manager/AccessoryManager+ToRadio.swift b/Meshtastic/Accessory/Accessory Manager/AccessoryManager+ToRadio.swift index 4fe2ffaf..868a6e6f 100644 --- a/Meshtastic/Accessory/Accessory Manager/AccessoryManager+ToRadio.swift +++ b/Meshtastic/Accessory/Accessory Manager/AccessoryManager+ToRadio.swift @@ -2110,4 +2110,34 @@ extension AccessoryManager { try await sendAdminMessageToRadio(meshPacket: meshPacket, adminDescription: messageDescription) } + + public func exchangeUserInfo(fromUser: UserEntity, toUser: UserEntity) async throws -> Int64 { + + let userProto = fromUser.toProto() + guard let userPayload: Data = try? userProto.serializedData() else { + throw AccessoryError.ioFailed("exchangeUserInfo: Unable to serialize User protobuf") + } + + var dataMessage = DataMessage() + dataMessage.payload = userPayload + dataMessage.portnum = PortNum.nodeinfoApp + dataMessage.wantResponse = true + + var meshPacket: MeshPacket = MeshPacket() + meshPacket.to = UInt32(toUser.num) + meshPacket.from = UInt32(fromUser.num) + meshPacket.id = UInt32.random(in: UInt32(UInt8.max).. AsyncStream - func disconnect(withError: Error?, shouldReconnect: Bool) throws + func disconnect(withError: Error?, shouldReconnect: Bool) async throws func drainPendingPackets() async throws func startDrainPendingPackets() throws diff --git a/Meshtastic/Accessory/Protocols/Transport.swift b/Meshtastic/Accessory/Protocols/Transport.swift index 55fa8545..af291869 100644 --- a/Meshtastic/Accessory/Protocols/Transport.swift +++ b/Meshtastic/Accessory/Protocols/Transport.swift @@ -42,10 +42,10 @@ enum DiscoveryEvent { protocol Transport { var type: TransportType { get } - var status: TransportStatus { get } + var status: TransportStatus { get async } // Discovers devices asynchronously. For ongoing scans (e.g., BLE), this can yield via AsyncStream. - func discoverDevices() -> AsyncStream + func discoverDevices() async -> AsyncStream // Connects to a device and returns a Connection. func connect(to device: Device) async throws -> any Connection diff --git a/Meshtastic/Accessory/Transports/Bluetooth Low Energy/BLEConnection.swift b/Meshtastic/Accessory/Transports/Bluetooth Low Energy/BLEConnection.swift index f7a0f012..a1513061 100644 --- a/Meshtastic/Accessory/Transports/Bluetooth Low Energy/BLEConnection.swift +++ b/Meshtastic/Accessory/Transports/Bluetooth Low Energy/BLEConnection.swift @@ -69,7 +69,7 @@ actor BLEConnection: Connection { self.delegate.setConnection(self) } - func disconnect(withError error: Error? = nil, shouldReconnect: Bool) throws { + func disconnect(withError error: Error? = nil, shouldReconnect: Bool) async throws { if peripheral.state == .connected { if let characteristic = FROMRADIO_characteristic { peripheral.setNotifyValue(false, for: characteristic) @@ -82,7 +82,7 @@ actor BLEConnection: Connection { } } - transport?.connectionDidDisconnect(fromPeripheral: peripheral) + await transport?.connectionDidDisconnect(fromPeripheral: peripheral) central.cancelPeripheralConnection(peripheral) peripheral.delegate = nil @@ -217,8 +217,8 @@ actor BLEConnection: Connection { self.connectContinuation = nil } - private func notifyTransportOfDisconnect() { - transport?.connectionDidDisconnect(fromPeripheral: peripheral) + private func notifyTransportOfDisconnect() async { + await transport?.connectionDidDisconnect(fromPeripheral: peripheral) } func startRSSITask() { @@ -450,7 +450,7 @@ actor BLEConnection: Connection { } // Inform the active connection that there was an error and it should disconnect - try self.disconnect(withError: error, shouldReconnect: shouldReconnect) + try await self.disconnect(withError: error, shouldReconnect: shouldReconnect) } func appDidEnterBackground() { diff --git a/Meshtastic/Accessory/Transports/Bluetooth Low Energy/BLETransport.swift b/Meshtastic/Accessory/Transports/Bluetooth Low Energy/BLETransport.swift index aa1a32d4..fc4953ac 100644 --- a/Meshtastic/Accessory/Transports/Bluetooth Low Energy/BLETransport.swift +++ b/Meshtastic/Accessory/Transports/Bluetooth Low Energy/BLETransport.swift @@ -10,7 +10,7 @@ import Foundation import SwiftUI import OSLog -class BLETransport: Transport { +actor BLETransport: Transport { let meshtasticServiceCBUUID = CBUUID(string: "0x6BA1B218-15A8-461F-9FA8-5DCAE273EAFD") private let kCentralRestoreID = "com.meshtastic.central" @@ -31,7 +31,7 @@ class BLETransport: Transport { private var cleanupTask: Task? // Transport properties - var supportsManualConnection: Bool = false + let supportsManualConnection: Bool = false let requiresPeriodicHeartbeat = false init() { @@ -46,19 +46,24 @@ class BLETransport: Transport { self.delegate.setTransport(self) } - nonisolated func discoverDevices() -> AsyncStream { + private func setDiscoveredDeviceContinuation(_ cont: AsyncStream.Continuation?) { + self.discoveredDeviceContinuation = cont + } + + func discoverDevices() -> AsyncStream { AsyncStream { cont in Task { - self.discoveredDeviceContinuation = cont + await self.setDiscoveredDeviceContinuation(cont) // This gate is opened when the CBCentralManager is in poweredOn state. // Its probably open already, but just to be sure in case we get here too quickly. try await self.setupCompleteGate.wait() - if !restoreInProgress { + if await !self.restoreInProgress { centralManager.scanForPeripherals(withServices: [meshtasticServiceCBUUID], options: [CBCentralManagerScanOptionAllowDuplicatesKey: true]) - for alreadyDiscoveredPeripheral in self.discoveredPeripherals.values.map({$0.peripheral}) { + let peripherals = await self.discoveredPeripherals.values.map({$0.peripheral}) + for alreadyDiscoveredPeripheral in peripherals { let device = Device(id: alreadyDiscoveredPeripheral.identifier, name: alreadyDiscoveredPeripheral.name ?? "Unknown", transportType: .ble, @@ -66,11 +71,13 @@ class BLETransport: Transport { cont.yield(.deviceFound(device)) } } - setupCleanupTask() + await setupCleanupTask() } cont.onTermination = { _ in Logger.transport.error("🛜 [BLE] Discovery event stream has been canecelled.") - self.stopScanning() + Task { + await self.stopScanning() + } } } } @@ -188,6 +195,12 @@ class BLETransport: Transport { } } + private func cancelConnectContinuation(for peripheral: CBPeripheral) { + self.connectContinuation?.resume(throwing: CancellationError()) + self.connectContinuation = nil + self.connectionDidDisconnect(fromPeripheral: peripheral) + } + func connect(to device: Device) async throws -> any Connection { guard let peripheral = discoveredPeripherals[UUID(uuidString: device.identifier)!] else { throw AccessoryError.connectionFailed("Peripheral not found") @@ -211,9 +224,9 @@ class BLETransport: Transport { self.activeConnection = newConnection return newConnection } onCancel: { - self.connectContinuation?.resume(throwing: CancellationError()) - self.connectContinuation = nil - self.connectionDidDisconnect(fromPeripheral: peripheral.peripheral) + Task { + await self.cancelConnectContinuation(for: peripheral.peripheral) + } } Logger.transport.debug("🛜 [BLE] Connect complete.") return returnConnection @@ -226,7 +239,7 @@ class BLETransport: Transport { Task { if await connection.peripheral.identifier == peripheral.identifier { try await connection.disconnect(withError: AccessoryError.disconnected("BLE connection lost"), shouldReconnect: true) - self.connectionDidDisconnect(fromPeripheral: peripheral) + await self.connectionDidDisconnect(fromPeripheral: peripheral) } } } @@ -264,7 +277,7 @@ class BLETransport: Transport { Logger.transport.debug("🛜 [BLETransport] Error while connecting. Disconnecting the active connection.") Task { try? await activeConnection.disconnect(withError: error, shouldReconnect: shouldReconnect) - self.connectionDidDisconnect(fromPeripheral: peripheral) + await self.connectionDidDisconnect(fromPeripheral: peripheral) } } else { Logger.transport.error("🚨 [BLETransport] unhandled error. May be in an inconsistent state.") @@ -372,15 +385,20 @@ class BLETransport: Transport { } Logger.transport.error("🛜 [BLE] Restoring peripheral in connecting state. ✅ didConnect Received!") - Task { @MainActor in - // In this case we need a full reconnect, so do the wantConfig, wantDatabase, and versionCheck - try? await AccessoryManager.shared.connect(to: device, withConnection: restoredConnection, wantConfig: true, wantDatabase: true, versionCheck: true) - restoreInProgress = false + let connectTask = Task { @MainActor in + try await AccessoryManager.shared.connect(to: device, withConnection: restoredConnection, wantConfig: true, wantDatabase: true, versionCheck: true) } + + do { + try await connectTask.value + } catch { + Logger.transport.error("🛜 [BLE] Error connecting during state restoration: \(error, privacy: .public)") + } + self.restoreInProgress = false } catch { - // We had a conneciton failure during restoration. + // We had a connection failure during restoration. Logger.transport.error("🛜 [BLE] Error restoring peripheral in connecting state. \(error, privacy: .public)") - restoreInProgress = false + self.restoreInProgress = false } } @@ -388,22 +406,28 @@ class BLETransport: Transport { let restoredConnection = BLEConnection(peripheral: peripheral, central: central, transport: self) self.activeConnection = restoredConnection Logger.transport.error("🛜 [BLE] Peripheral Connection found and state is connected setting this connection as the activeConnection.") - Task { @MainActor in + let connectTask = Task { @MainActor in // In this case we need a full reconnect, so do the wantConfig, wantDatabase, and versionCheck - try? await AccessoryManager.shared.connect(to: device, withConnection: restoredConnection, wantConfig: false, wantDatabase: false, versionCheck: false) - restoreInProgress = false + try await AccessoryManager.shared.connect(to: device, withConnection: restoredConnection, wantConfig: false, wantDatabase: false, versionCheck: false) } + do { + try await connectTask.value + } catch { + Logger.transport.error("🛜 [BLE] Error connecting during state restoration: \(error, privacy: .public)") + } + + self.restoreInProgress = false Logger.transport.error("🛜 [BLE] Connection state successfully restored in the background.") default: // Since we're not going to attempt to reconnect in then allow normal device discovery Logger.transport.error("🛜 [BLE] Unhandled state restoration for state: \(cbPeripheralStateDescription(peripheral.state), privacy: .public).") - restoreInProgress = false + self.restoreInProgress = false } } } - func device(forManualConnection: String) -> Device? { + nonisolated func device(forManualConnection: String) -> Device? { return nil } @@ -438,33 +462,33 @@ class BLEDelegate: NSObject, CBCentralManagerDelegate { } func centralManagerDidUpdateState(_ central: CBCentralManager) { - transport?.handleCentralState(central.state, central: central) + Task { await transport?.handleCentralState(central.state, central: central) } } func centralManager(_ central: CBCentralManager, didDiscover peripheral: CBPeripheral, advertisementData: [String: Any], rssi RSSI: NSNumber) { - transport?.didDiscover(peripheral: peripheral, rssi: RSSI) + Task { await transport?.didDiscover(peripheral: peripheral, rssi: RSSI) } } func centralManager(_ central: CBCentralManager, didConnect peripheral: CBPeripheral) { - transport?.handleDidConnect(peripheral: peripheral, central: central) + Task { await transport?.handleDidConnect(peripheral: peripheral, central: central) } } func centralManager(_ central: CBCentralManager, didFailToConnect peripheral: CBPeripheral, error: Error?) { - transport?.handleDidFailToConnect(peripheral: peripheral, error: error) + Task { await transport?.handleDidFailToConnect(peripheral: peripheral, error: error) } } func centralManager(_ central: CBCentralManager, didDisconnectPeripheral peripheral: CBPeripheral, error: Error?) { if let error = error as? NSError { Logger.transport.error("🛜 [BLETransport] Error while disconnecting peripheral: \(peripheral.name ?? ""): \(error)") - transport?.handlePeripheralDisconnectError(peripheral: peripheral, error: error) + Task { await transport?.handlePeripheralDisconnectError(peripheral: peripheral, error: error) } } else { Logger.transport.error("🛜 [BLETransport] Did succesfully disconnect peripheral: \(peripheral.name ?? "")") - transport?.handlePeripheralDisconnect(peripheral: peripheral) + Task { await transport?.handlePeripheralDisconnect(peripheral: peripheral) } } } func centralManager(_ central: CBCentralManager, willRestoreState dict: [String: Any]) { - self.transport?.handleWillRestoreState(dict: dict, central: central) + Task { await self.transport?.handleWillRestoreState(dict: dict, central: central) } } } diff --git a/Meshtastic/Extensions/UIKeyboardType.swift b/Meshtastic/Extensions/UIKeyboardType.swift new file mode 100644 index 00000000..1353e35e --- /dev/null +++ b/Meshtastic/Extensions/UIKeyboardType.swift @@ -0,0 +1,13 @@ +// +// UIKeyboard.swift +// Meshtastic +// +// Copyright(c) Garth Vander Houwen 1/7/26. +// +import UIKit + +extension UIKeyboardType { + static var emoji: UIKeyboardType { + return UIKeyboardType(rawValue: 124) ?? .default + } +} diff --git a/Meshtastic/Extensions/UserDefaults.swift b/Meshtastic/Extensions/UserDefaults.swift index 82e67773..12bd86ee 100644 --- a/Meshtastic/Extensions/UserDefaults.swift +++ b/Meshtastic/Extensions/UserDefaults.swift @@ -80,6 +80,7 @@ extension UserDefaults { case showDeviceOnboarding case usageDataAndCrashReporting case autoconnectOnDiscovery + case purgeStaleNodeDays case manualConnections case testIntEnum } @@ -178,6 +179,9 @@ extension UserDefaults { @UserDefault(.autoconnectOnDiscovery, defaultValue: true) static var autoconnectOnDiscovery: Bool + @UserDefault(.purgeStaleNodeDays, defaultValue: 0) + static var purgeStaleNodeDays: Double + @UserDefault(.testIntEnum, defaultValue: .one) static var testIntEnum: TestIntEnum diff --git a/Meshtastic/Helpers/EmojiOnlyTextField.swift b/Meshtastic/Helpers/EmojiOnlyTextField.swift index 0982ab33..aae9e3a3 100644 --- a/Meshtastic/Helpers/EmojiOnlyTextField.swift +++ b/Meshtastic/Helpers/EmojiOnlyTextField.swift @@ -7,6 +7,7 @@ import SwiftUI class SwiftUIEmojiTextField: UITextField { + var shouldBecomeFirstResponderOnAppear = false func setEmoji() { _ = self.textInputMode @@ -23,22 +24,39 @@ class SwiftUIEmojiTextField: UITextField { } return nil } + + override func didMoveToWindow() { + super.didMoveToWindow() + if shouldBecomeFirstResponderOnAppear && window != nil { + DispatchQueue.main.async { [weak self] in + self?.becomeFirstResponder() + } + } + } } struct EmojiOnlyTextField: UIViewRepresentable { @Binding var text: String var placeholder: String = "" + var onBecomeFirstResponder: (() -> Void)? + var onKeyboardTypeChanged: ((Bool) -> Void)? // true if NOT emoji (should dismiss), false if emoji + var onKeyboardDismissed: (() -> Void)? // Called when keyboard is dismissed func makeUIView(context: Context) -> SwiftUIEmojiTextField { let emojiTextField = SwiftUIEmojiTextField() emojiTextField.placeholder = placeholder emojiTextField.text = text emojiTextField.delegate = context.coordinator + emojiTextField.shouldBecomeFirstResponderOnAppear = true + context.coordinator.textField = emojiTextField return emojiTextField } func updateUIView(_ uiView: SwiftUIEmojiTextField, context: Context) { uiView.text = text + context.coordinator.onBecomeFirstResponder = onBecomeFirstResponder + context.coordinator.onKeyboardTypeChanged = onKeyboardTypeChanged + context.coordinator.onKeyboardDismissed = onKeyboardDismissed } func makeCoordinator() -> Coordinator { @@ -47,13 +65,41 @@ struct EmojiOnlyTextField: UIViewRepresentable { class Coordinator: NSObject, UITextFieldDelegate { var parent: EmojiOnlyTextField + var textField: SwiftUIEmojiTextField? + var onBecomeFirstResponder: (() -> Void)? + var onKeyboardTypeChanged: ((Bool) -> Void)? + var onKeyboardDismissed: (() -> Void)? + var previousInputMode: String? + init(parent: EmojiOnlyTextField) { self.parent = parent } + + func textFieldDidBeginEditing(_ textField: UITextField) { + onBecomeFirstResponder?() + checkInputMode(textField) + } + + func textFieldDidEndEditing(_ textField: UITextField) { + // Keyboard was dismissed + onKeyboardDismissed?() + } + func textFieldDidChangeSelection(_ textField: UITextField) { DispatchQueue.main.async { [weak self] in self?.parent.text = textField.text ?? "" } + checkInputMode(textField) + } + + private func checkInputMode(_ textField: UITextField) { + if let inputMode = textField.textInputMode { + let isEmoji = inputMode.primaryLanguage == "emoji" + if previousInputMode != inputMode.primaryLanguage { + previousInputMode = inputMode.primaryLanguage + onKeyboardTypeChanged?(!isEmoji) // true if NOT emoji (should dismiss) + } + } } } } diff --git a/Meshtastic/Helpers/MeshPackets.swift b/Meshtastic/Helpers/MeshPackets.swift index 255417c4..8b7a7423 100644 --- a/Meshtastic/Helpers/MeshPackets.swift +++ b/Meshtastic/Helpers/MeshPackets.swift @@ -881,8 +881,8 @@ func telemetryPacket(packet: MeshPacket, connectedNode: Int64, context: NSManage let meshActivity = Activity.activities.first(where: { $0.attributes.nodeNum == connectedNode }) if meshActivity != nil { Task { - await meshActivity?.update(updatedContent, alertConfiguration: alertConfiguration) - // await meshActivity?.update(updatedContent) + // await meshActivity?.update(updatedContent, alertConfiguration: alertConfiguration) + await meshActivity?.update(updatedContent) Logger.services.debug("Updated live activity.") } } diff --git a/Meshtastic/Views/Messages/MessageContextMenuItems.swift b/Meshtastic/Views/Messages/MessageContextMenuItems.swift index 63104320..0d8843ef 100644 --- a/Meshtastic/Views/Messages/MessageContextMenuItems.swift +++ b/Meshtastic/Views/Messages/MessageContextMenuItems.swift @@ -10,6 +10,7 @@ struct MessageContextMenuItems: View { let tapBackDestination: MessageDestination let isCurrentUser: Bool @Binding var isShowingDeleteConfirmation: Bool + @Binding var isShowingTapbackInput: Bool let onReply: () -> Void @State var relayDisplay: String? = nil @@ -29,29 +30,10 @@ struct MessageContextMenuItems: View { } } - Menu("Tapback") { - ForEach(Tapbacks.allCases) { tb in - Button { - Task { - do { - try await accessoryManager.sendMessage( - message: tb.emojiString, - toUserNum: tapBackDestination.userNum, - channel: tapBackDestination.channelNum, - isEmoji: true, - replyID: message.messageId - ) - Task { @MainActor in - self.context.refresh(tapBackDestination.managedObject, mergeChanges: true) - } - } catch { - Logger.services.warning("Failed to send tapback.") - } - } - } label: { - Text(tb.description) - Image(uiImage: tb.emojiString.image()!) - } + Button("Tapback") { + // The context menu needs a moment to dismiss before the focus state can be changed. + DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { + isShowingTapbackInput = true } } diff --git a/Meshtastic/Views/Messages/MessageText.swift b/Meshtastic/Views/Messages/MessageText.swift index 28df8fba..afdda5a3 100644 --- a/Meshtastic/Views/Messages/MessageText.swift +++ b/Meshtastic/Views/Messages/MessageText.swift @@ -28,101 +28,15 @@ struct MessageText: View { @State private var saveChannelLink: SaveChannelLinkData? @State private var isShowingDeleteConfirmation = false + @FocusState private var isTapbackInputFocused: Bool + @State private var tapbackText = "" + var body: some View { - SessionReplayPrivacyView(textAndInputPrivacy: .maskAll) { - - let markdownText = LocalizedStringKey(message.messagePayloadMarkdown ?? (message.messagePayload ?? "EMPTY MESSAGE")) - return Text(markdownText) - .tint(Self.linkBlue) - .padding(.vertical, 10) - .padding(.horizontal, 8) - .foregroundColor(.white) - .background(isCurrentUser ? .accentColor : Color(.gray)) - .cornerRadius(15) - .overlay { - /// Show the lock if the message is pki encrypted and has a real ack if sent by the current user, or is pki encrypted for incoming messages - if message.pkiEncrypted && message.realACK || !isCurrentUser && message.pkiEncrypted { - VStack(alignment: .trailing) { - Spacer() - HStack { - Spacer() - Image(systemName: "lock.circle.fill") - .symbolRenderingMode(.palette) - .foregroundStyle(.white, .green) - .font(.system(size: 20)) - .offset(x: 8, y: 8) - } - } - } - let isStoreAndForward = message.portNum == Int32(PortNum.storeForwardApp.rawValue) - let isDetectionSensorMessage = message.portNum == Int32(PortNum.detectionSensorApp.rawValue) - if isStoreAndForward { - VStack(alignment: .trailing) { - Spacer() - HStack { - Spacer() - Image(systemName: "envelope.circle.fill") - .symbolRenderingMode(.palette) - .foregroundStyle(.white, .gray) - .font(.system(size: 20)) - .offset(x: 8, y: 8) - } - } - } - if tapBackDestination.overlaySensorMessage { - VStack { - isDetectionSensorMessage ? Image(systemName: "sensor.fill") - .padding() - .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topTrailing) - .foregroundStyle(Color.orange) - .symbolRenderingMode(.multicolor) - .symbolEffect(.variableColor.reversing.cumulative, options: .repeat(20).speed(3)) - .offset(x: 20, y: -20) - : nil - } - } else { - EmptyView() - } - } - .contextMenu { - MessageContextMenuItems( - message: message, - tapBackDestination: tapBackDestination, - isCurrentUser: isCurrentUser, - isShowingDeleteConfirmation: $isShowingDeleteConfirmation, - onReply: onReply - ) - } + messageContent .environment(\.openURL, OpenURLAction { url in - saveChannelLink = nil - var addChannels = false - if url.absoluteString.lowercased().contains("meshtastic.org/v/#") { - // Handle contact URL - ContactURLHandler.handleContactUrl(url: url, accessoryManager: AccessoryManager.shared) - return .handled // Prevent default browser opening - } else if url.absoluteString.lowercased().contains("meshtastic.org/e/") { - // Handle channel URL - let components = url.absoluteString.components(separatedBy: "#") - guard !components.isEmpty, let lastComponent = components.last else { - Logger.services.error("No valid components found in channel URL: \(url.absoluteString, privacy: .public)") - return .discarded - } - addChannels = Bool(url.query?.contains("add=true") ?? false) - guard let lastComponent = components.last else { - Logger.services.error("Channel URL missing fragment component: \(url.absoluteString, privacy: .public)") - self.saveChannelLink = nil - return .discarded - } - let cs = lastComponent.components(separatedBy: "?").first ?? "" - self.saveChannelLink = SaveChannelLinkData(data: cs, add: addChannels) - Logger.services.debug("Add Channel: \(addChannels, privacy: .public)") - Logger.mesh.debug("Opening Channel Settings URL: \(url.absoluteString, privacy: .public)") - return .handled // Prevent default browser opening - } - return .systemAction // Open other URLs in browser + handleURL(url) }) - // Display sheet for channel settings .sheet(item: $saveChannelLink) { link in SaveChannelQRCode( channelSetLink: link.data, @@ -138,17 +52,155 @@ struct MessageText: View { titleVisibility: .visible ) { Button("Delete Message", role: .destructive) { - context.delete(message) - do { - try context.save() - } catch { - Logger.data.error("Failed to delete message \(message.messageId, privacy: .public): \(error.localizedDescription, privacy: .public)") - } + deleteMessage() } Button("Cancel", role: .cancel) {} } } } + + private var messageContent: some View { + let markdownText = LocalizedStringKey(message.messagePayloadMarkdown ?? (message.messagePayload ?? "EMPTY MESSAGE")) + return Text(markdownText) + .tint(Self.linkBlue) + .padding(.vertical, 10) + .padding(.horizontal, 8) + .foregroundColor(.white) + .background(isCurrentUser ? .accentColor : Color(.gray)) + .cornerRadius(15) + .background { + TextField("", text: $tapbackText) + .keyboardType(.emoji) + .scrollDismissesKeyboard(.immediately) + .focused($isTapbackInputFocused) + .frame(width: 0, height: 0) + .opacity(0) + .onChange(of: tapbackText) { + processTapback() + } + } + .overlay(messageOverlays) + .contextMenu { + MessageContextMenuItems( + message: message, + tapBackDestination: tapBackDestination, + isCurrentUser: isCurrentUser, + isShowingDeleteConfirmation: $isShowingDeleteConfirmation, + isShowingTapbackInput: Binding( + get: { isTapbackInputFocused }, + set: { isTapbackInputFocused = $0 } + ), + onReply: onReply + ) + } + } + + @ViewBuilder + private var messageOverlays: some View { + if message.pkiEncrypted && message.realACK || !isCurrentUser && message.pkiEncrypted { + VStack(alignment: .trailing) { + Spacer() + HStack { + Spacer() + Image(systemName: "lock.circle.fill") + .symbolRenderingMode(.palette) + .foregroundStyle(.white, .green) + .font(.system(size: 20)) + .offset(x: 8, y: 8) + } + } + } + if message.portNum == Int32(PortNum.storeForwardApp.rawValue) { + VStack(alignment: .trailing) { + Spacer() + HStack { + Spacer() + Image(systemName: "envelope.circle.fill") + .symbolRenderingMode(.palette) + .foregroundStyle(.white, .gray) + .font(.system(size: 20)) + .offset(x: 8, y: 8) + } + } + } + if tapBackDestination.overlaySensorMessage && message.portNum == Int32(PortNum.detectionSensorApp.rawValue) { + Image(systemName: "sensor.fill") + .padding() + .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topTrailing) + .foregroundStyle(Color.orange) + .symbolRenderingMode(.multicolor) + .symbolEffect(.variableColor.reversing.cumulative, options: .repeat(20).speed(3)) + .offset(x: 20, y: -20) + } + } + + private func handleURL(_ url: URL) -> OpenURLAction.Result { + saveChannelLink = nil + var addChannels = false + if url.absoluteString.lowercased().contains("meshtastic.org/v/#") { + // Handle contact URL + ContactURLHandler.handleContactUrl(url: url, accessoryManager: AccessoryManager.shared) + return .handled // Prevent default browser opening + } else if url.absoluteString.lowercased().contains("meshtastic.org/e/") { + // Handle channel URL + let components = url.absoluteString.components(separatedBy: "#") + guard !components.isEmpty, let lastComponent = components.last else { + Logger.services.error("No valid components found in channel URL: \(url.absoluteString, privacy: .public)") + return .discarded + } + addChannels = Bool(url.query?.contains("add=true") ?? false) + guard let lastComponent = components.last else { + Logger.services.error("Channel URL missing fragment component: \(url.absoluteString, privacy: .public)") + self.saveChannelLink = nil + return .discarded + } + let cs = lastComponent.components(separatedBy: "?").first ?? "" + self.saveChannelLink = SaveChannelLinkData(data: cs, add: addChannels) + Logger.services.debug("Add Channel: \(addChannels, privacy: .public)") + Logger.mesh.debug("Opening Channel Settings URL: \(url.absoluteString, privacy: .public)") + return .handled // Prevent default browser opening + } + return .systemAction // Open other URLs in browser + } + + private func deleteMessage() { + context.delete(message) + do { + try context.save() + } catch { + Logger.data.error("Failed to delete message \(message.messageId, privacy: .public): \(error.localizedDescription, privacy: .public)") + } + } + + private func processTapback() { + guard !tapbackText.isEmpty else { return } + let emojiToSend = tapbackText + + Task { + do { + try await accessoryManager.sendMessage( + message: emojiToSend, + toUserNum: tapBackDestination.userNum, + channel: tapBackDestination.channelNum, + isEmoji: true, + replyID: message.messageId + ) + await MainActor.run { + switch tapBackDestination { + case let .channel(channel): + context.refresh(channel, mergeChanges: true) + case let .user(user): + context.refresh(user, mergeChanges: true) + } + } + } catch { + Logger.services.warning("Failed to send tapback.") + } + } + + tapbackText = "" + isTapbackInputFocused = false + } } private extension MessageDestination { diff --git a/Meshtastic/Views/Messages/TapbackInputView.swift b/Meshtastic/Views/Messages/TapbackInputView.swift new file mode 100644 index 00000000..bf6de5a6 --- /dev/null +++ b/Meshtastic/Views/Messages/TapbackInputView.swift @@ -0,0 +1,80 @@ +import SwiftUI +import UIKit + +struct TapbackInputView: View { + @Binding var text: String + @Binding var isPresented: Bool + let onEmojiSelected: (String) -> Void + + var body: some View { + NavigationView { + VStack(spacing: 0) { + TextField("Tap to enter emoji", text: $text) + .keyboardType(.emoji) + .frame(height: 50) + .padding(.horizontal) + .background( + RoundedRectangle(cornerRadius: 10) + .strokeBorder(.tertiary, lineWidth: 1) + .background(RoundedRectangle(cornerRadius: 10).fill(Color(.systemBackground))) + ) + .padding(.horizontal) + .padding(.top, 8) + .onChange(of: text) { oldValue, newValue in + // Extract first emoji character and send it + if !newValue.isEmpty, let firstEmoji = extractFirstEmoji(from: newValue) { + onEmojiSelected(firstEmoji) + // Clear the text box after getting the emoji + text = "" + } + } + } + .navigationTitle("Tapback") + .navigationBarTitleDisplayMode(.inline) + .toolbar { + ToolbarItem(placement: .navigationBarTrailing) { + Button("Cancel") { + isPresented = false + } + } + } + } + .presentationDetents([.height(120)]) + } + + private func extractFirstEmoji(from string: String) -> String? { + // Extract the first emoji character(s) - handle both single and multi-scalar emojis + guard !string.isEmpty else { return nil } + + // Try to get the first character + let firstChar = string[string.startIndex] + + // Check if it's an emoji using the existing extension + if firstChar.isEmoji { + // For multi-scalar emojis (like emojis with skin tones), we need to find the full emoji sequence + var emojiEnd = string.index(after: string.startIndex) + + // Check if there are continuation scalars (for emojis with skin tones, variation selectors, etc.) + while emojiEnd < string.endIndex { + let nextChar = string[emojiEnd] + // Check if this is a continuation (variation selector, skin tone modifier, zero-width joiner, etc.) + if let scalar = nextChar.unicodeScalars.first, + (scalar.properties.isVariationSelector || + scalar.value == 0xFE0F || // Variation selector + (scalar.value >= 0x1F3FB && scalar.value <= 0x1F3FF) || // Skin tone modifiers + scalar.value == 0x200D) { // Zero-width joiner + emojiEnd = string.index(after: emojiEnd) + } else if nextChar.isEmoji { + // If it's another emoji, include it (for compound emojis like flags) + emojiEnd = string.index(after: emojiEnd) + } else { + break + } + } + + return String(string[string.startIndex.. = [.medium, .fraction(0.85)] @State private var selectedDetent: PresentationDetent = .medium @State private var waypointFailedAlert: Bool = false @@ -111,26 +110,19 @@ struct WaypointForm: View { HStack { Text("Icon") Spacer() - EmojiOnlyTextField(text: $icon, placeholder: "Select an emoji") + TextField("Select an emoji", text: $icon) + .keyboardType(.emoji) .font(.title) .focused($iconIsFocused) .onChange(of: icon) { _, value in - - // If you have anything other than emojis in your string make it empty - if !value.onlyEmojis() { - icon = "" - } // If a second emoji is entered delete the first one if value.count >= 1 { - if value.count > 1 { let index = value.index(value.startIndex, offsetBy: 1) icon = String(value[index]) } - iconIsFocused = false } } - } Toggle(isOn: $expires) { Label("Expires", systemImage: "clock.badge.xmark") @@ -458,7 +450,6 @@ struct WaypointForm: View { longitude = waypoint.coordinate.longitude } } - .presentationDetents(detents, selection: $selectedDetent) .presentationBackgroundInteraction(.enabled(upThrough: .fraction(0.85))) .presentationDragIndicator(.visible) } diff --git a/Meshtastic/Views/Nodes/Helpers/NodeDetail.swift b/Meshtastic/Views/Nodes/Helpers/NodeDetail.swift index dc394f35..660e57bd 100644 --- a/Meshtastic/Views/Nodes/Helpers/NodeDetail.swift +++ b/Meshtastic/Views/Nodes/Helpers/NodeDetail.swift @@ -464,6 +464,10 @@ struct NodeDetail: View { node: node, connectedNode: connectedNode ) + ExchangeUserInfoButton( + node: node, + connectedNode: connectedNode + ) TraceRouteButton( node: node ) diff --git a/Meshtastic/Views/Nodes/MeshMap.swift b/Meshtastic/Views/Nodes/MeshMap.swift index d72987da..3e268afa 100644 --- a/Meshtastic/Views/Nodes/MeshMap.swift +++ b/Meshtastic/Views/Nodes/MeshMap.swift @@ -120,16 +120,14 @@ struct MeshMap: View { } .sheet(item: $selectedWaypoint) { selection in WaypointForm(waypoint: selection) - .padding() + .presentationDetents([.large]) } .sheet(item: $editingWaypoint) { selection in WaypointForm(waypoint: selection, editMode: true) - .padding() + .presentationDetents([.large]) } .sheet(isPresented: $editingSettings) { MapSettingsForm(traffic: $showTraffic, pointsOfInterest: $showPointsOfInterest, mapLayer: $selectedMapLayer, meshMap: $isMeshMap, enabledOverlayConfigs: $enabledOverlayConfigs) - .presentationDetents([.large]) - } .onChange(of: router.navigationState) { guard case .map = router.navigationState.selectedTab else { return } diff --git a/Meshtastic/Views/Nodes/NodeList.swift b/Meshtastic/Views/Nodes/NodeList.swift index f324ed8b..c751f84a 100644 --- a/Meshtastic/Views/Nodes/NodeList.swift +++ b/Meshtastic/Views/Nodes/NodeList.swift @@ -253,6 +253,19 @@ fileprivate struct FilteredNodeList: View { } label: { Label("Exchange Positions", systemImage: "arrow.triangle.2.circlepath") } + Button { + Task { + if let fromUser = connectedNode.user, let toUser = node.user { + do { + _ = try await accessoryManager.exchangeUserInfo(fromUser: fromUser, toUser: toUser) + } catch { + Logger.mesh.warning("Failed to exchange user info") + } + } + } + } label: { + Label("Exchange User Info", systemImage: "person.2.badge.gearshape") + } TraceRouteButton( node: node ) diff --git a/Meshtastic/Views/Settings/AppSettings.swift b/Meshtastic/Views/Settings/AppSettings.swift index 2f10c2af..243325b1 100644 --- a/Meshtastic/Views/Settings/AppSettings.swift +++ b/Meshtastic/Views/Settings/AppSettings.swift @@ -120,7 +120,7 @@ struct AppSettings: View { Text("180") } } - Text("Favorited and ignored nodes are always retained. Nodes without PKC keys are cleared from the app database on the schedule set by the user, nodes with PKC keys are cleared only if the interval is set to 7 days or longer. This feature only purges nodes from the app that are not stored in the device node database.") + Text("Favorited and ignored nodes are always retained. Other nodes are cleared from the app database on the schedule set by the user. (Nodes with PKC keys are always retained for at least 7 days.) This feature only purges nodes from the app that are not stored in the device node database.") .foregroundStyle(.secondary) .font(idiom == .phone ? .caption : .callout) } diff --git a/MeshtasticProtobufs/Sources/meshtastic/admin.pb.swift b/MeshtasticProtobufs/Sources/meshtastic/admin.pb.swift index fa6a9e61..1bb9c2ce 100644 --- a/MeshtasticProtobufs/Sources/meshtastic/admin.pb.swift +++ b/MeshtasticProtobufs/Sources/meshtastic/admin.pb.swift @@ -21,6 +21,55 @@ fileprivate struct _GeneratedWithProtocGenSwiftVersion: SwiftProtobuf.ProtobufAP typealias Version = _2 } +/// +/// Firmware update mode for OTA updates +public enum OTAMode: SwiftProtobuf.Enum, Swift.CaseIterable { + public typealias RawValue = Int + + /// + /// Do not reboot into OTA mode + case noRebootOta // = 0 + + /// + /// Reboot into OTA mode for BLE firmware update + case otaBle // = 1 + + /// + /// Reboot into OTA mode for WiFi firmware update + case otaWifi // = 2 + case UNRECOGNIZED(Int) + + public init() { + self = .noRebootOta + } + + public init?(rawValue: Int) { + switch rawValue { + case 0: self = .noRebootOta + case 1: self = .otaBle + case 2: self = .otaWifi + default: self = .UNRECOGNIZED(rawValue) + } + } + + public var rawValue: Int { + switch self { + case .noRebootOta: return 0 + case .otaBle: return 1 + case .otaWifi: return 2 + case .UNRECOGNIZED(let i): return i + } + } + + // The compiler won't synthesize support with the UNRECOGNIZED case. + public static let allCases: [OTAMode] = [ + .noRebootOta, + .otaBle, + .otaWifi, + ] + +} + /// /// This message is handled by the Admin module and is responsible for all settings/channel read/write operations. /// This message is used to do settings operations to both remote AND local nodes. @@ -478,6 +527,16 @@ public struct AdminMessage: Sendable { set {payloadVariant = .removeIgnoredNode(newValue)} } + /// + /// Set specified node-num to be muted + public var toggleMutedNode: UInt32 { + get { + if case .toggleMutedNode(let v)? = payloadVariant {return v} + return 0 + } + set {payloadVariant = .toggleMutedNode(newValue)} + } + /// /// Begins an edit transaction for config, module config, owner, and channel settings changes /// This will delay the standard *implicit* save to the file system and subsequent reboot behavior until committed (commit_edit_settings) @@ -532,6 +591,9 @@ public struct AdminMessage: Sendable { /// /// Tell the node to reboot into the OTA Firmware in this many seconds (or <0 to cancel reboot) /// Only Implemented for ESP32 Devices. This needs to be issued to send a new main firmware via bluetooth. + /// Deprecated in favor of reboot_ota_mode in 2.7.17 + /// + /// NOTE: This field was marked as deprecated in the .proto file. public var rebootOtaSeconds: Int32 { get { if case .rebootOtaSeconds(let v)? = payloadVariant {return v} @@ -592,6 +654,16 @@ public struct AdminMessage: Sendable { set {payloadVariant = .nodedbReset(newValue)} } + /// + /// Tell the node to reset into the OTA Loader + public var otaRequest: AdminMessage.OTAEvent { + get { + if case .otaRequest(let v)? = payloadVariant {return v} + return AdminMessage.OTAEvent() + } + set {payloadVariant = .otaRequest(newValue)} + } + public var unknownFields = SwiftProtobuf.UnknownStorage() /// @@ -735,6 +807,9 @@ public struct AdminMessage: Sendable { /// Set specified node-num to be un-ignored on the NodeDB on the device case removeIgnoredNode(UInt32) /// + /// Set specified node-num to be muted + case toggleMutedNode(UInt32) + /// /// Begins an edit transaction for config, module config, owner, and channel settings changes /// This will delay the standard *implicit* save to the file system and subsequent reboot behavior until committed (commit_edit_settings) case beginEditSettings(Bool) @@ -753,6 +828,9 @@ public struct AdminMessage: Sendable { /// /// Tell the node to reboot into the OTA Firmware in this many seconds (or <0 to cancel reboot) /// Only Implemented for ESP32 Devices. This needs to be issued to send a new main firmware via bluetooth. + /// Deprecated in favor of reboot_ota_mode in 2.7.17 + /// + /// NOTE: This field was marked as deprecated in the .proto file. case rebootOtaSeconds(Int32) /// /// This message is only supported for the simulator Portduino build. @@ -771,6 +849,9 @@ public struct AdminMessage: Sendable { /// Tell the node to reset the nodedb. /// When true, favorites are preserved through reset. case nodedbReset(Bool) + /// + /// Tell the node to reset into the OTA Loader + case otaRequest(AdminMessage.OTAEvent) } @@ -1059,6 +1140,28 @@ public struct AdminMessage: Sendable { public init() {} } + /// + /// User is requesting an over the air update. + /// Node will reboot into the OTA loader + public struct OTAEvent: Sendable { + // SwiftProtobuf.Message conformance is added in an extension below. See the + // `Message` and `Message+*Additions` files in the SwiftProtobuf library for + // methods supported on all messages. + + /// + /// Tell the node to reboot into OTA mode for firmware update via BLE or WiFi (ESP32 only for now) + public var rebootOtaMode: OTAMode = .noRebootOta + + /// + /// A 32 byte hash of the OTA firmware. + /// Used to verify the integrity of the firmware before applying an update. + public var otaHash: Data = Data() + + public var unknownFields = SwiftProtobuf.UnknownStorage() + + public init() {} + } + public init() {} } @@ -1239,9 +1342,13 @@ public struct KeyVerificationAdmin: Sendable { fileprivate let _protobuf_package = "meshtastic" +extension OTAMode: SwiftProtobuf._ProtoNameProviding { + public static let _protobuf_nameMap = SwiftProtobuf._NameMap(bytecode: "\0\u{2}\0NO_REBOOT_OTA\0\u{1}OTA_BLE\0\u{1}OTA_WIFI\0") +} + extension AdminMessage: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { public static let protoMessageName: String = _protobuf_package + ".AdminMessage" - public static let _protobuf_nameMap = SwiftProtobuf._NameMap(bytecode: "\0\u{3}get_channel_request\0\u{3}get_channel_response\0\u{3}get_owner_request\0\u{3}get_owner_response\0\u{3}get_config_request\0\u{3}get_config_response\0\u{3}get_module_config_request\0\u{3}get_module_config_response\0\u{4}\u{2}get_canned_message_module_messages_request\0\u{3}get_canned_message_module_messages_response\0\u{3}get_device_metadata_request\0\u{3}get_device_metadata_response\0\u{3}get_ringtone_request\0\u{3}get_ringtone_response\0\u{3}get_device_connection_status_request\0\u{3}get_device_connection_status_response\0\u{3}set_ham_mode\0\u{3}get_node_remote_hardware_pins_request\0\u{3}get_node_remote_hardware_pins_response\0\u{3}enter_dfu_mode_request\0\u{3}delete_file_request\0\u{3}set_scale\0\u{3}backup_preferences\0\u{3}restore_preferences\0\u{3}remove_backup_preferences\0\u{3}send_input_event\0\u{4}\u{5}set_owner\0\u{3}set_channel\0\u{3}set_config\0\u{3}set_module_config\0\u{3}set_canned_message_module_messages\0\u{3}set_ringtone_message\0\u{3}remove_by_nodenum\0\u{3}set_favorite_node\0\u{3}remove_favorite_node\0\u{3}set_fixed_position\0\u{3}remove_fixed_position\0\u{3}set_time_only\0\u{3}get_ui_config_request\0\u{3}get_ui_config_response\0\u{3}store_ui_config\0\u{3}set_ignored_node\0\u{3}remove_ignored_node\0\u{4}\u{10}begin_edit_settings\0\u{3}commit_edit_settings\0\u{3}add_contact\0\u{3}key_verification\0\u{4}\u{1b}factory_reset_device\0\u{3}reboot_ota_seconds\0\u{3}exit_simulator\0\u{3}reboot_seconds\0\u{3}shutdown_seconds\0\u{3}factory_reset_config\0\u{3}nodedb_reset\0\u{3}session_passkey\0") + public static let _protobuf_nameMap = SwiftProtobuf._NameMap(bytecode: "\0\u{3}get_channel_request\0\u{3}get_channel_response\0\u{3}get_owner_request\0\u{3}get_owner_response\0\u{3}get_config_request\0\u{3}get_config_response\0\u{3}get_module_config_request\0\u{3}get_module_config_response\0\u{4}\u{2}get_canned_message_module_messages_request\0\u{3}get_canned_message_module_messages_response\0\u{3}get_device_metadata_request\0\u{3}get_device_metadata_response\0\u{3}get_ringtone_request\0\u{3}get_ringtone_response\0\u{3}get_device_connection_status_request\0\u{3}get_device_connection_status_response\0\u{3}set_ham_mode\0\u{3}get_node_remote_hardware_pins_request\0\u{3}get_node_remote_hardware_pins_response\0\u{3}enter_dfu_mode_request\0\u{3}delete_file_request\0\u{3}set_scale\0\u{3}backup_preferences\0\u{3}restore_preferences\0\u{3}remove_backup_preferences\0\u{3}send_input_event\0\u{4}\u{5}set_owner\0\u{3}set_channel\0\u{3}set_config\0\u{3}set_module_config\0\u{3}set_canned_message_module_messages\0\u{3}set_ringtone_message\0\u{3}remove_by_nodenum\0\u{3}set_favorite_node\0\u{3}remove_favorite_node\0\u{3}set_fixed_position\0\u{3}remove_fixed_position\0\u{3}set_time_only\0\u{3}get_ui_config_request\0\u{3}get_ui_config_response\0\u{3}store_ui_config\0\u{3}set_ignored_node\0\u{3}remove_ignored_node\0\u{3}toggle_muted_node\0\u{4}\u{f}begin_edit_settings\0\u{3}commit_edit_settings\0\u{3}add_contact\0\u{3}key_verification\0\u{4}\u{1b}factory_reset_device\0\u{3}reboot_ota_seconds\0\u{3}exit_simulator\0\u{3}reboot_seconds\0\u{3}shutdown_seconds\0\u{3}factory_reset_config\0\u{3}nodedb_reset\0\u{3}session_passkey\0\u{3}ota_request\0") public mutating func decodeMessage(decoder: inout D) throws { while let fieldNumber = try decoder.nextFieldNumber() { @@ -1673,6 +1780,14 @@ extension AdminMessage: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementat self.payloadVariant = .removeIgnoredNode(v) } }() + case 49: try { + var v: UInt32? + try decoder.decodeSingularUInt32Field(value: &v) + if let v = v { + if self.payloadVariant != nil {try decoder.handleConflictingOneOf()} + self.payloadVariant = .toggleMutedNode(v) + } + }() case 64: try { var v: Bool? try decoder.decodeSingularBoolField(value: &v) @@ -1772,6 +1887,19 @@ extension AdminMessage: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementat } }() case 101: try { try decoder.decodeSingularBytesField(value: &self.sessionPasskey) }() + case 102: try { + var v: AdminMessage.OTAEvent? + var hadOneofValue = false + if let current = self.payloadVariant { + hadOneofValue = true + if case .otaRequest(let m) = current {v = m} + } + try decoder.decodeSingularMessageField(value: &v) + if let v = v { + if hadOneofValue {try decoder.handleConflictingOneOf()} + self.payloadVariant = .otaRequest(v) + } + }() default: break } } @@ -1955,6 +2083,10 @@ extension AdminMessage: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementat guard case .removeIgnoredNode(let v)? = self.payloadVariant else { preconditionFailure() } try visitor.visitSingularUInt32Field(value: v, fieldNumber: 48) }() + case .toggleMutedNode?: try { + guard case .toggleMutedNode(let v)? = self.payloadVariant else { preconditionFailure() } + try visitor.visitSingularUInt32Field(value: v, fieldNumber: 49) + }() case .beginEditSettings?: try { guard case .beginEditSettings(let v)? = self.payloadVariant else { preconditionFailure() } try visitor.visitSingularBoolField(value: v, fieldNumber: 64) @@ -1999,11 +2131,14 @@ extension AdminMessage: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementat guard case .nodedbReset(let v)? = self.payloadVariant else { preconditionFailure() } try visitor.visitSingularBoolField(value: v, fieldNumber: 100) }() - case nil: break + default: break } if !self.sessionPasskey.isEmpty { try visitor.visitSingularBytesField(value: self.sessionPasskey, fieldNumber: 101) } + try { if case .otaRequest(let v)? = self.payloadVariant { + try visitor.visitSingularMessageField(value: v, fieldNumber: 102) + } }() try unknownFields.traverse(visitor: &visitor) } @@ -2072,6 +2207,41 @@ extension AdminMessage.InputEvent: SwiftProtobuf.Message, SwiftProtobuf._Message } } +extension AdminMessage.OTAEvent: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { + public static let protoMessageName: String = AdminMessage.protoMessageName + ".OTAEvent" + public static let _protobuf_nameMap = SwiftProtobuf._NameMap(bytecode: "\0\u{3}reboot_ota_mode\0\u{3}ota_hash\0") + + public mutating func decodeMessage(decoder: inout D) throws { + while let fieldNumber = try decoder.nextFieldNumber() { + // The use of inline closures is to circumvent an issue where the compiler + // allocates stack space for every case branch when no optimizations are + // enabled. https://github.com/apple/swift-protobuf/issues/1034 + switch fieldNumber { + case 1: try { try decoder.decodeSingularEnumField(value: &self.rebootOtaMode) }() + case 2: try { try decoder.decodeSingularBytesField(value: &self.otaHash) }() + default: break + } + } + } + + public func traverse(visitor: inout V) throws { + if self.rebootOtaMode != .noRebootOta { + try visitor.visitSingularEnumField(value: self.rebootOtaMode, fieldNumber: 1) + } + if !self.otaHash.isEmpty { + try visitor.visitSingularBytesField(value: self.otaHash, fieldNumber: 2) + } + try unknownFields.traverse(visitor: &visitor) + } + + public static func ==(lhs: AdminMessage.OTAEvent, rhs: AdminMessage.OTAEvent) -> Bool { + if lhs.rebootOtaMode != rhs.rebootOtaMode {return false} + if lhs.otaHash != rhs.otaHash {return false} + if lhs.unknownFields != rhs.unknownFields {return false} + return true + } +} + extension HamParameters: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { public static let protoMessageName: String = _protobuf_package + ".HamParameters" public static let _protobuf_nameMap = SwiftProtobuf._NameMap(bytecode: "\0\u{3}call_sign\0\u{3}tx_power\0\u{1}frequency\0\u{3}short_name\0") diff --git a/MeshtasticProtobufs/Sources/meshtastic/config.pb.swift b/MeshtasticProtobufs/Sources/meshtastic/config.pb.swift index 28074a6b..5dddccd7 100644 --- a/MeshtasticProtobufs/Sources/meshtastic/config.pb.swift +++ b/MeshtasticProtobufs/Sources/meshtastic/config.pb.swift @@ -281,7 +281,7 @@ public struct Config: Sendable { case routerLate // = 11 /// - /// Description: Treats packets from or to favorited nodes as ROUTER, and all other packets as CLIENT. + /// Description: Treats packets from or to favorited nodes as ROUTER_LATE, and all other packets as CLIENT. /// Technical Details: Used for stronger attic/roof nodes to distribute messages more widely /// from weaker, indoor, or less-well-positioned nodes. Recommended for users with multiple nodes /// where one CLIENT_BASE acts as a more powerful base station, such as an attic/roof node. diff --git a/MeshtasticProtobufs/Sources/meshtastic/deviceonly.pb.swift b/MeshtasticProtobufs/Sources/meshtastic/deviceonly.pb.swift index 281f18d8..cee33f81 100644 --- a/MeshtasticProtobufs/Sources/meshtastic/deviceonly.pb.swift +++ b/MeshtasticProtobufs/Sources/meshtastic/deviceonly.pb.swift @@ -230,6 +230,7 @@ public struct NodeInfoLite: @unchecked Sendable { /// /// Bitfield for storing booleans. /// LSB 0 is_key_manually_verified + /// LSB 1 is_muted public var bitfield: UInt32 { get {return _storage._bitfield} set {_uniqueStorage()._bitfield = newValue} diff --git a/MeshtasticProtobufs/Sources/meshtastic/mesh.pb.swift b/MeshtasticProtobufs/Sources/meshtastic/mesh.pb.swift index 477e2457..e8be5add 100644 --- a/MeshtasticProtobufs/Sources/meshtastic/mesh.pb.swift +++ b/MeshtasticProtobufs/Sources/meshtastic/mesh.pb.swift @@ -166,8 +166,8 @@ public enum HardwareModel: SwiftProtobuf.Enum, Swift.CaseIterable { case loraRelayV1 // = 32 /// - /// TODO: REPLACE - case nrf52840Dk // = 33 + /// T-Echo Plus device from LilyGo + case tEchoPlus // = 33 /// /// TODO: REPLACE @@ -535,6 +535,18 @@ public enum HardwareModel: SwiftProtobuf.Enum, Swift.CaseIterable { /// Elecrow ThinkNode M6 case thinknodeM6 // = 120 + /// + /// Elecrow Meshstick 1262 + case meshstick1262 // = 121 + + /// + /// LilyGo T-Beam 1W + case tbeam1Watt // = 122 + + /// + /// LilyGo T5 S3 ePaper Pro (V1 and V2) + case t5S3EpaperPro // = 123 + /// /// ------------------------------------------------------------------------------------------------------------------------------------------ /// Reserved ID For developing private Ports. These will show up in live traffic sparsely, so we can use a high number. Keep it within 8 bits. @@ -581,7 +593,7 @@ public enum HardwareModel: SwiftProtobuf.Enum, Swift.CaseIterable { case 30: self = .rp2040Lora case 31: self = .stationG2 case 32: self = .loraRelayV1 - case 33: self = .nrf52840Dk + case 33: self = .tEchoPlus case 34: self = .ppr case 35: self = .genieblocks case 36: self = .nrf52Unknown @@ -669,6 +681,9 @@ public enum HardwareModel: SwiftProtobuf.Enum, Swift.CaseIterable { case 118: self = .rak6421 case 119: self = .thinknodeM4 case 120: self = .thinknodeM6 + case 121: self = .meshstick1262 + case 122: self = .tbeam1Watt + case 123: self = .t5S3EpaperPro case 255: self = .privateHw default: self = .UNRECOGNIZED(rawValue) } @@ -709,7 +724,7 @@ public enum HardwareModel: SwiftProtobuf.Enum, Swift.CaseIterable { case .rp2040Lora: return 30 case .stationG2: return 31 case .loraRelayV1: return 32 - case .nrf52840Dk: return 33 + case .tEchoPlus: return 33 case .ppr: return 34 case .genieblocks: return 35 case .nrf52Unknown: return 36 @@ -797,6 +812,9 @@ public enum HardwareModel: SwiftProtobuf.Enum, Swift.CaseIterable { case .rak6421: return 118 case .thinknodeM4: return 119 case .thinknodeM6: return 120 + case .meshstick1262: return 121 + case .tbeam1Watt: return 122 + case .t5S3EpaperPro: return 123 case .privateHw: return 255 case .UNRECOGNIZED(let i): return i } @@ -837,7 +855,7 @@ public enum HardwareModel: SwiftProtobuf.Enum, Swift.CaseIterable { .rp2040Lora, .stationG2, .loraRelayV1, - .nrf52840Dk, + .tEchoPlus, .ppr, .genieblocks, .nrf52Unknown, @@ -925,6 +943,9 @@ public enum HardwareModel: SwiftProtobuf.Enum, Swift.CaseIterable { .rak6421, .thinknodeM4, .thinknodeM6, + .meshstick1262, + .tbeam1Watt, + .t5S3EpaperPro, .privateHw, ] @@ -1921,6 +1942,11 @@ public struct Routing: Sendable { /// Airtime fairness rate limit exceeded for a packet /// This typically enforced per portnum and is used to prevent a single node from monopolizing airtime case rateLimitExceeded // = 38 + + /// + /// PKI encryption failed, due to no public key for the remote node + /// This is different from PKI_UNKNOWN_PUBKEY which indicates a failure upon receiving a packet + case pkiSendFailPublicKey // = 39 case UNRECOGNIZED(Int) public init() { @@ -1946,6 +1972,7 @@ public struct Routing: Sendable { case 36: self = .adminBadSessionKey case 37: self = .adminPublicKeyUnauthorized case 38: self = .rateLimitExceeded + case 39: self = .pkiSendFailPublicKey default: self = .UNRECOGNIZED(rawValue) } } @@ -1969,6 +1996,7 @@ public struct Routing: Sendable { case .adminBadSessionKey: return 36 case .adminPublicKeyUnauthorized: return 37 case .rateLimitExceeded: return 38 + case .pkiSendFailPublicKey: return 39 case .UNRECOGNIZED(let i): return i } } @@ -1992,6 +2020,7 @@ public struct Routing: Sendable { .adminBadSessionKey, .adminPublicKeyUnauthorized, .rateLimitExceeded, + .pkiSendFailPublicKey, ] } @@ -2136,6 +2165,10 @@ public struct StoreForwardPlusPlus: Sendable { /// The receive time of the message in question public var encapsulatedRxtime: UInt32 = 0 + /// + /// Used in a LINK_REQUEST to specify the message X spots back from head + public var chainCount: UInt32 = 0 + public var unknownFields = SwiftProtobuf.UnknownStorage() /// @@ -2936,6 +2969,14 @@ public struct NodeInfo: @unchecked Sendable { set {_uniqueStorage()._isKeyManuallyVerified = newValue} } + /// + /// True if node has been muted + /// Persistes between NodeDB internal clean ups + public var isMuted: Bool { + get {return _storage._isMuted} + set {_uniqueStorage()._isMuted = newValue} + } + public var unknownFields = SwiftProtobuf.UnknownStorage() public init() {} @@ -3959,7 +4000,7 @@ public struct ChunkedPayloadResponse: Sendable { fileprivate let _protobuf_package = "meshtastic" extension HardwareModel: SwiftProtobuf._ProtoNameProviding { - public static let _protobuf_nameMap = SwiftProtobuf._NameMap(bytecode: "\0\u{2}\0UNSET\0\u{1}TLORA_V2\0\u{1}TLORA_V1\0\u{1}TLORA_V2_1_1P6\0\u{1}TBEAM\0\u{1}HELTEC_V2_0\0\u{1}TBEAM_V0P7\0\u{1}T_ECHO\0\u{1}TLORA_V1_1P3\0\u{1}RAK4631\0\u{1}HELTEC_V2_1\0\u{1}HELTEC_V1\0\u{1}LILYGO_TBEAM_S3_CORE\0\u{1}RAK11200\0\u{1}NANO_G1\0\u{1}TLORA_V2_1_1P8\0\u{1}TLORA_T3_S3\0\u{1}NANO_G1_EXPLORER\0\u{1}NANO_G2_ULTRA\0\u{1}LORA_TYPE\0\u{1}WIPHONE\0\u{1}WIO_WM1110\0\u{1}RAK2560\0\u{1}HELTEC_HRU_3601\0\u{1}HELTEC_WIRELESS_BRIDGE\0\u{1}STATION_G1\0\u{1}RAK11310\0\u{1}SENSELORA_RP2040\0\u{1}SENSELORA_S3\0\u{1}CANARYONE\0\u{1}RP2040_LORA\0\u{1}STATION_G2\0\u{1}LORA_RELAY_V1\0\u{1}NRF52840DK\0\u{1}PPR\0\u{1}GENIEBLOCKS\0\u{1}NRF52_UNKNOWN\0\u{1}PORTDUINO\0\u{1}ANDROID_SIM\0\u{1}DIY_V1\0\u{1}NRF52840_PCA10059\0\u{1}DR_DEV\0\u{1}M5STACK\0\u{1}HELTEC_V3\0\u{1}HELTEC_WSL_V3\0\u{1}BETAFPV_2400_TX\0\u{1}BETAFPV_900_NANO_TX\0\u{1}RPI_PICO\0\u{1}HELTEC_WIRELESS_TRACKER\0\u{1}HELTEC_WIRELESS_PAPER\0\u{1}T_DECK\0\u{1}T_WATCH_S3\0\u{1}PICOMPUTER_S3\0\u{1}HELTEC_HT62\0\u{1}EBYTE_ESP32_S3\0\u{1}ESP32_S3_PICO\0\u{1}CHATTER_2\0\u{1}HELTEC_WIRELESS_PAPER_V1_0\0\u{1}HELTEC_WIRELESS_TRACKER_V1_0\0\u{1}UNPHONE\0\u{1}TD_LORAC\0\u{1}CDEBYTE_EORA_S3\0\u{1}TWC_MESH_V4\0\u{1}NRF52_PROMICRO_DIY\0\u{1}RADIOMASTER_900_BANDIT_NANO\0\u{1}HELTEC_CAPSULE_SENSOR_V3\0\u{1}HELTEC_VISION_MASTER_T190\0\u{1}HELTEC_VISION_MASTER_E213\0\u{1}HELTEC_VISION_MASTER_E290\0\u{1}HELTEC_MESH_NODE_T114\0\u{1}SENSECAP_INDICATOR\0\u{1}TRACKER_T1000_E\0\u{1}RAK3172\0\u{1}WIO_E5\0\u{1}RADIOMASTER_900_BANDIT\0\u{1}ME25LS01_4Y10TD\0\u{1}RP2040_FEATHER_RFM95\0\u{1}M5STACK_COREBASIC\0\u{1}M5STACK_CORE2\0\u{1}RPI_PICO2\0\u{1}M5STACK_CORES3\0\u{1}SEEED_XIAO_S3\0\u{1}MS24SF1\0\u{1}TLORA_C6\0\u{1}WISMESH_TAP\0\u{1}ROUTASTIC\0\u{1}MESH_TAB\0\u{1}MESHLINK\0\u{1}XIAO_NRF52_KIT\0\u{1}THINKNODE_M1\0\u{1}THINKNODE_M2\0\u{1}T_ETH_ELITE\0\u{1}HELTEC_SENSOR_HUB\0\u{1}MUZI_BASE\0\u{1}HELTEC_MESH_POCKET\0\u{1}SEEED_SOLAR_NODE\0\u{1}NOMADSTAR_METEOR_PRO\0\u{1}CROWPANEL\0\u{1}LINK_32\0\u{1}SEEED_WIO_TRACKER_L1\0\u{1}SEEED_WIO_TRACKER_L1_EINK\0\u{1}MUZI_R1_NEO\0\u{1}T_DECK_PRO\0\u{1}T_LORA_PAGER\0\u{1}M5STACK_RESERVED\0\u{1}WISMESH_TAG\0\u{1}RAK3312\0\u{1}THINKNODE_M5\0\u{1}HELTEC_MESH_SOLAR\0\u{1}T_ECHO_LITE\0\u{1}HELTEC_V4\0\u{1}M5STACK_C6L\0\u{1}M5STACK_CARDPUTER_ADV\0\u{1}HELTEC_WIRELESS_TRACKER_V2\0\u{1}T_WATCH_ULTRA\0\u{1}THINKNODE_M3\0\u{1}WISMESH_TAP_V2\0\u{1}RAK3401\0\u{1}RAK6421\0\u{1}THINKNODE_M4\0\u{1}THINKNODE_M6\0\u{2}G\u{2}PRIVATE_HW\0") + public static let _protobuf_nameMap = SwiftProtobuf._NameMap(bytecode: "\0\u{2}\0UNSET\0\u{1}TLORA_V2\0\u{1}TLORA_V1\0\u{1}TLORA_V2_1_1P6\0\u{1}TBEAM\0\u{1}HELTEC_V2_0\0\u{1}TBEAM_V0P7\0\u{1}T_ECHO\0\u{1}TLORA_V1_1P3\0\u{1}RAK4631\0\u{1}HELTEC_V2_1\0\u{1}HELTEC_V1\0\u{1}LILYGO_TBEAM_S3_CORE\0\u{1}RAK11200\0\u{1}NANO_G1\0\u{1}TLORA_V2_1_1P8\0\u{1}TLORA_T3_S3\0\u{1}NANO_G1_EXPLORER\0\u{1}NANO_G2_ULTRA\0\u{1}LORA_TYPE\0\u{1}WIPHONE\0\u{1}WIO_WM1110\0\u{1}RAK2560\0\u{1}HELTEC_HRU_3601\0\u{1}HELTEC_WIRELESS_BRIDGE\0\u{1}STATION_G1\0\u{1}RAK11310\0\u{1}SENSELORA_RP2040\0\u{1}SENSELORA_S3\0\u{1}CANARYONE\0\u{1}RP2040_LORA\0\u{1}STATION_G2\0\u{1}LORA_RELAY_V1\0\u{1}T_ECHO_PLUS\0\u{1}PPR\0\u{1}GENIEBLOCKS\0\u{1}NRF52_UNKNOWN\0\u{1}PORTDUINO\0\u{1}ANDROID_SIM\0\u{1}DIY_V1\0\u{1}NRF52840_PCA10059\0\u{1}DR_DEV\0\u{1}M5STACK\0\u{1}HELTEC_V3\0\u{1}HELTEC_WSL_V3\0\u{1}BETAFPV_2400_TX\0\u{1}BETAFPV_900_NANO_TX\0\u{1}RPI_PICO\0\u{1}HELTEC_WIRELESS_TRACKER\0\u{1}HELTEC_WIRELESS_PAPER\0\u{1}T_DECK\0\u{1}T_WATCH_S3\0\u{1}PICOMPUTER_S3\0\u{1}HELTEC_HT62\0\u{1}EBYTE_ESP32_S3\0\u{1}ESP32_S3_PICO\0\u{1}CHATTER_2\0\u{1}HELTEC_WIRELESS_PAPER_V1_0\0\u{1}HELTEC_WIRELESS_TRACKER_V1_0\0\u{1}UNPHONE\0\u{1}TD_LORAC\0\u{1}CDEBYTE_EORA_S3\0\u{1}TWC_MESH_V4\0\u{1}NRF52_PROMICRO_DIY\0\u{1}RADIOMASTER_900_BANDIT_NANO\0\u{1}HELTEC_CAPSULE_SENSOR_V3\0\u{1}HELTEC_VISION_MASTER_T190\0\u{1}HELTEC_VISION_MASTER_E213\0\u{1}HELTEC_VISION_MASTER_E290\0\u{1}HELTEC_MESH_NODE_T114\0\u{1}SENSECAP_INDICATOR\0\u{1}TRACKER_T1000_E\0\u{1}RAK3172\0\u{1}WIO_E5\0\u{1}RADIOMASTER_900_BANDIT\0\u{1}ME25LS01_4Y10TD\0\u{1}RP2040_FEATHER_RFM95\0\u{1}M5STACK_COREBASIC\0\u{1}M5STACK_CORE2\0\u{1}RPI_PICO2\0\u{1}M5STACK_CORES3\0\u{1}SEEED_XIAO_S3\0\u{1}MS24SF1\0\u{1}TLORA_C6\0\u{1}WISMESH_TAP\0\u{1}ROUTASTIC\0\u{1}MESH_TAB\0\u{1}MESHLINK\0\u{1}XIAO_NRF52_KIT\0\u{1}THINKNODE_M1\0\u{1}THINKNODE_M2\0\u{1}T_ETH_ELITE\0\u{1}HELTEC_SENSOR_HUB\0\u{1}MUZI_BASE\0\u{1}HELTEC_MESH_POCKET\0\u{1}SEEED_SOLAR_NODE\0\u{1}NOMADSTAR_METEOR_PRO\0\u{1}CROWPANEL\0\u{1}LINK_32\0\u{1}SEEED_WIO_TRACKER_L1\0\u{1}SEEED_WIO_TRACKER_L1_EINK\0\u{1}MUZI_R1_NEO\0\u{1}T_DECK_PRO\0\u{1}T_LORA_PAGER\0\u{1}M5STACK_RESERVED\0\u{1}WISMESH_TAG\0\u{1}RAK3312\0\u{1}THINKNODE_M5\0\u{1}HELTEC_MESH_SOLAR\0\u{1}T_ECHO_LITE\0\u{1}HELTEC_V4\0\u{1}M5STACK_C6L\0\u{1}M5STACK_CARDPUTER_ADV\0\u{1}HELTEC_WIRELESS_TRACKER_V2\0\u{1}T_WATCH_ULTRA\0\u{1}THINKNODE_M3\0\u{1}WISMESH_TAP_V2\0\u{1}RAK3401\0\u{1}RAK6421\0\u{1}THINKNODE_M4\0\u{1}THINKNODE_M6\0\u{1}MESHSTICK_1262\0\u{1}TBEAM_1_WATT\0\u{1}T5_S3_EPAPER_PRO\0\u{2}D\u{2}PRIVATE_HW\0") } extension Constants: SwiftProtobuf._ProtoNameProviding { @@ -4409,7 +4450,7 @@ extension Routing: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBa } extension Routing.Error: SwiftProtobuf._ProtoNameProviding { - public static let _protobuf_nameMap = SwiftProtobuf._NameMap(bytecode: "\0\u{2}\0NONE\0\u{1}NO_ROUTE\0\u{1}GOT_NAK\0\u{1}TIMEOUT\0\u{1}NO_INTERFACE\0\u{1}MAX_RETRANSMIT\0\u{1}NO_CHANNEL\0\u{1}TOO_LARGE\0\u{1}NO_RESPONSE\0\u{1}DUTY_CYCLE_LIMIT\0\u{2}\u{17}BAD_REQUEST\0\u{1}NOT_AUTHORIZED\0\u{1}PKI_FAILED\0\u{1}PKI_UNKNOWN_PUBKEY\0\u{1}ADMIN_BAD_SESSION_KEY\0\u{1}ADMIN_PUBLIC_KEY_UNAUTHORIZED\0\u{1}RATE_LIMIT_EXCEEDED\0") + public static let _protobuf_nameMap = SwiftProtobuf._NameMap(bytecode: "\0\u{2}\0NONE\0\u{1}NO_ROUTE\0\u{1}GOT_NAK\0\u{1}TIMEOUT\0\u{1}NO_INTERFACE\0\u{1}MAX_RETRANSMIT\0\u{1}NO_CHANNEL\0\u{1}TOO_LARGE\0\u{1}NO_RESPONSE\0\u{1}DUTY_CYCLE_LIMIT\0\u{2}\u{17}BAD_REQUEST\0\u{1}NOT_AUTHORIZED\0\u{1}PKI_FAILED\0\u{1}PKI_UNKNOWN_PUBKEY\0\u{1}ADMIN_BAD_SESSION_KEY\0\u{1}ADMIN_PUBLIC_KEY_UNAUTHORIZED\0\u{1}RATE_LIMIT_EXCEEDED\0\u{1}PKI_SEND_FAIL_PUBLIC_KEY\0") } extension DataMessage: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { @@ -4528,7 +4569,7 @@ extension KeyVerification: SwiftProtobuf.Message, SwiftProtobuf._MessageImplemen extension StoreForwardPlusPlus: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { public static let protoMessageName: String = _protobuf_package + ".StoreForwardPlusPlus" - public static let _protobuf_nameMap = SwiftProtobuf._NameMap(bytecode: "\0\u{3}sfpp_message_type\0\u{3}message_hash\0\u{3}commit_hash\0\u{3}root_hash\0\u{1}message\0\u{3}encapsulated_id\0\u{3}encapsulated_to\0\u{3}encapsulated_from\0\u{3}encapsulated_rxtime\0") + public static let _protobuf_nameMap = SwiftProtobuf._NameMap(bytecode: "\0\u{3}sfpp_message_type\0\u{3}message_hash\0\u{3}commit_hash\0\u{3}root_hash\0\u{1}message\0\u{3}encapsulated_id\0\u{3}encapsulated_to\0\u{3}encapsulated_from\0\u{3}encapsulated_rxtime\0\u{3}chain_count\0") public mutating func decodeMessage(decoder: inout D) throws { while let fieldNumber = try decoder.nextFieldNumber() { @@ -4545,6 +4586,7 @@ extension StoreForwardPlusPlus: SwiftProtobuf.Message, SwiftProtobuf._MessageImp case 7: try { try decoder.decodeSingularUInt32Field(value: &self.encapsulatedTo) }() case 8: try { try decoder.decodeSingularUInt32Field(value: &self.encapsulatedFrom) }() case 9: try { try decoder.decodeSingularUInt32Field(value: &self.encapsulatedRxtime) }() + case 10: try { try decoder.decodeSingularUInt32Field(value: &self.chainCount) }() default: break } } @@ -4578,6 +4620,9 @@ extension StoreForwardPlusPlus: SwiftProtobuf.Message, SwiftProtobuf._MessageImp if self.encapsulatedRxtime != 0 { try visitor.visitSingularUInt32Field(value: self.encapsulatedRxtime, fieldNumber: 9) } + if self.chainCount != 0 { + try visitor.visitSingularUInt32Field(value: self.chainCount, fieldNumber: 10) + } try unknownFields.traverse(visitor: &visitor) } @@ -4591,6 +4636,7 @@ extension StoreForwardPlusPlus: SwiftProtobuf.Message, SwiftProtobuf._MessageImp if lhs.encapsulatedTo != rhs.encapsulatedTo {return false} if lhs.encapsulatedFrom != rhs.encapsulatedFrom {return false} if lhs.encapsulatedRxtime != rhs.encapsulatedRxtime {return false} + if lhs.chainCount != rhs.chainCount {return false} if lhs.unknownFields != rhs.unknownFields {return false} return true } @@ -4981,7 +5027,7 @@ extension MeshPacket.TransportMechanism: SwiftProtobuf._ProtoNameProviding { extension NodeInfo: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { public static let protoMessageName: String = _protobuf_package + ".NodeInfo" - public static let _protobuf_nameMap = SwiftProtobuf._NameMap(bytecode: "\0\u{1}num\0\u{1}user\0\u{1}position\0\u{1}snr\0\u{3}last_heard\0\u{3}device_metrics\0\u{1}channel\0\u{3}via_mqtt\0\u{3}hops_away\0\u{3}is_favorite\0\u{3}is_ignored\0\u{3}is_key_manually_verified\0") + public static let _protobuf_nameMap = SwiftProtobuf._NameMap(bytecode: "\0\u{1}num\0\u{1}user\0\u{1}position\0\u{1}snr\0\u{3}last_heard\0\u{3}device_metrics\0\u{1}channel\0\u{3}via_mqtt\0\u{3}hops_away\0\u{3}is_favorite\0\u{3}is_ignored\0\u{3}is_key_manually_verified\0\u{3}is_muted\0") fileprivate class _StorageClass { var _num: UInt32 = 0 @@ -4996,6 +5042,7 @@ extension NodeInfo: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationB var _isFavorite: Bool = false var _isIgnored: Bool = false var _isKeyManuallyVerified: Bool = false + var _isMuted: Bool = false // This property is used as the initial default value for new instances of the type. // The type itself is protecting the reference to its storage via CoW semantics. @@ -5018,6 +5065,7 @@ extension NodeInfo: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationB _isFavorite = source._isFavorite _isIgnored = source._isIgnored _isKeyManuallyVerified = source._isKeyManuallyVerified + _isMuted = source._isMuted } } @@ -5048,6 +5096,7 @@ extension NodeInfo: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationB case 10: try { try decoder.decodeSingularBoolField(value: &_storage._isFavorite) }() case 11: try { try decoder.decodeSingularBoolField(value: &_storage._isIgnored) }() case 12: try { try decoder.decodeSingularBoolField(value: &_storage._isKeyManuallyVerified) }() + case 13: try { try decoder.decodeSingularBoolField(value: &_storage._isMuted) }() default: break } } @@ -5096,6 +5145,9 @@ extension NodeInfo: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationB if _storage._isKeyManuallyVerified != false { try visitor.visitSingularBoolField(value: _storage._isKeyManuallyVerified, fieldNumber: 12) } + if _storage._isMuted != false { + try visitor.visitSingularBoolField(value: _storage._isMuted, fieldNumber: 13) + } } try unknownFields.traverse(visitor: &visitor) } @@ -5117,6 +5169,7 @@ extension NodeInfo: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationB if _storage._isFavorite != rhs_storage._isFavorite {return false} if _storage._isIgnored != rhs_storage._isIgnored {return false} if _storage._isKeyManuallyVerified != rhs_storage._isKeyManuallyVerified {return false} + if _storage._isMuted != rhs_storage._isMuted {return false} return true } if !storagesAreEqual {return false} diff --git a/MeshtasticProtobufs/Sources/meshtastic/module_config.pb.swift b/MeshtasticProtobufs/Sources/meshtastic/module_config.pb.swift index 1bff36a2..007440b4 100644 --- a/MeshtasticProtobufs/Sources/meshtastic/module_config.pb.swift +++ b/MeshtasticProtobufs/Sources/meshtastic/module_config.pb.swift @@ -805,9 +805,15 @@ public struct ModuleConfig: Sendable { /// https://beta.ivc.no/wiki/index.php/Victron_VE_Direct_DIY_Cable case veDirect // = 7 - ///Used to configure and view some parameters of MeshSolar. - ///https://heltec.org/project/meshsolar/ + /// Used to configure and view some parameters of MeshSolar. + /// https://heltec.org/project/meshsolar/ case msConfig // = 8 + + /// Logs mesh traffic to the serial pins, ideal for logging via openLog or similar. + case log // = 9 + + /// only text (channel & DM) + case logtext // = 10 case UNRECOGNIZED(Int) public init() { @@ -825,6 +831,8 @@ public struct ModuleConfig: Sendable { case 6: self = .ws85 case 7: self = .veDirect case 8: self = .msConfig + case 9: self = .log + case 10: self = .logtext default: self = .UNRECOGNIZED(rawValue) } } @@ -840,6 +848,8 @@ public struct ModuleConfig: Sendable { case .ws85: return 6 case .veDirect: return 7 case .msConfig: return 8 + case .log: return 9 + case .logtext: return 10 case .UNRECOGNIZED(let i): return i } } @@ -855,6 +865,8 @@ public struct ModuleConfig: Sendable { .ws85, .veDirect, .msConfig, + .log, + .logtext, ] } @@ -1080,6 +1092,10 @@ public struct ModuleConfig: Sendable { /// Note: We will still send telemtry to the connected phone / client every minute over the API public var deviceTelemetryEnabled: Bool = false + /// + /// Enable/Disable the air quality telemetry measurement module on-device display + public var airQualityScreenEnabled: Bool = false + public var unknownFields = SwiftProtobuf.UnknownStorage() public init() {} @@ -2005,7 +2021,7 @@ extension ModuleConfig.SerialConfig.Serial_Baud: SwiftProtobuf._ProtoNameProvidi } extension ModuleConfig.SerialConfig.Serial_Mode: SwiftProtobuf._ProtoNameProviding { - public static let _protobuf_nameMap = SwiftProtobuf._NameMap(bytecode: "\0\u{2}\0DEFAULT\0\u{1}SIMPLE\0\u{1}PROTO\0\u{1}TEXTMSG\0\u{1}NMEA\0\u{1}CALTOPO\0\u{1}WS85\0\u{1}VE_DIRECT\0\u{1}MS_CONFIG\0") + public static let _protobuf_nameMap = SwiftProtobuf._NameMap(bytecode: "\0\u{2}\0DEFAULT\0\u{1}SIMPLE\0\u{1}PROTO\0\u{1}TEXTMSG\0\u{1}NMEA\0\u{1}CALTOPO\0\u{1}WS85\0\u{1}VE_DIRECT\0\u{1}MS_CONFIG\0\u{1}LOG\0\u{1}LOGTEXT\0") } extension ModuleConfig.ExternalNotificationConfig: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { @@ -2210,7 +2226,7 @@ extension ModuleConfig.RangeTestConfig: SwiftProtobuf.Message, SwiftProtobuf._Me extension ModuleConfig.TelemetryConfig: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { public static let protoMessageName: String = ModuleConfig.protoMessageName + ".TelemetryConfig" - public static let _protobuf_nameMap = SwiftProtobuf._NameMap(bytecode: "\0\u{3}device_update_interval\0\u{3}environment_update_interval\0\u{3}environment_measurement_enabled\0\u{3}environment_screen_enabled\0\u{3}environment_display_fahrenheit\0\u{3}air_quality_enabled\0\u{3}air_quality_interval\0\u{3}power_measurement_enabled\0\u{3}power_update_interval\0\u{3}power_screen_enabled\0\u{3}health_measurement_enabled\0\u{3}health_update_interval\0\u{3}health_screen_enabled\0\u{3}device_telemetry_enabled\0") + public static let _protobuf_nameMap = SwiftProtobuf._NameMap(bytecode: "\0\u{3}device_update_interval\0\u{3}environment_update_interval\0\u{3}environment_measurement_enabled\0\u{3}environment_screen_enabled\0\u{3}environment_display_fahrenheit\0\u{3}air_quality_enabled\0\u{3}air_quality_interval\0\u{3}power_measurement_enabled\0\u{3}power_update_interval\0\u{3}power_screen_enabled\0\u{3}health_measurement_enabled\0\u{3}health_update_interval\0\u{3}health_screen_enabled\0\u{3}device_telemetry_enabled\0\u{3}air_quality_screen_enabled\0") public mutating func decodeMessage(decoder: inout D) throws { while let fieldNumber = try decoder.nextFieldNumber() { @@ -2232,6 +2248,7 @@ extension ModuleConfig.TelemetryConfig: SwiftProtobuf.Message, SwiftProtobuf._Me case 12: try { try decoder.decodeSingularUInt32Field(value: &self.healthUpdateInterval) }() case 13: try { try decoder.decodeSingularBoolField(value: &self.healthScreenEnabled) }() case 14: try { try decoder.decodeSingularBoolField(value: &self.deviceTelemetryEnabled) }() + case 15: try { try decoder.decodeSingularBoolField(value: &self.airQualityScreenEnabled) }() default: break } } @@ -2280,6 +2297,9 @@ extension ModuleConfig.TelemetryConfig: SwiftProtobuf.Message, SwiftProtobuf._Me if self.deviceTelemetryEnabled != false { try visitor.visitSingularBoolField(value: self.deviceTelemetryEnabled, fieldNumber: 14) } + if self.airQualityScreenEnabled != false { + try visitor.visitSingularBoolField(value: self.airQualityScreenEnabled, fieldNumber: 15) + } try unknownFields.traverse(visitor: &visitor) } @@ -2298,6 +2318,7 @@ extension ModuleConfig.TelemetryConfig: SwiftProtobuf.Message, SwiftProtobuf._Me if lhs.healthUpdateInterval != rhs.healthUpdateInterval {return false} if lhs.healthScreenEnabled != rhs.healthScreenEnabled {return false} if lhs.deviceTelemetryEnabled != rhs.deviceTelemetryEnabled {return false} + if lhs.airQualityScreenEnabled != rhs.airQualityScreenEnabled {return false} if lhs.unknownFields != rhs.unknownFields {return false} return true } diff --git a/protobufs b/protobufs index 62ef17b3..c8d5047b 160000 --- a/protobufs +++ b/protobufs @@ -1 +1 @@ -Subproject commit 62ef17b3d1625fc6d78ed661f614d0baad4be9ef +Subproject commit c8d5047b6351b732c0bccfcea6960a532f7ae49a From beb990e6ef7b6841a3d49e393bb43c0d4b8e22f7 Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Thu, 15 Jan 2026 16:47:12 -0800 Subject: [PATCH 4/4] Bump version --- Meshtastic.xcodeproj/project.pbxproj | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Meshtastic.xcodeproj/project.pbxproj b/Meshtastic.xcodeproj/project.pbxproj index 817570e2..f1c8b759 100644 --- a/Meshtastic.xcodeproj/project.pbxproj +++ b/Meshtastic.xcodeproj/project.pbxproj @@ -2110,7 +2110,7 @@ "@executable_path/Frameworks", ); MACOSX_DEPLOYMENT_TARGET = 14.6; - MARKETING_VERSION = 2.7.7; + MARKETING_VERSION = 2.7.8; PRODUCT_BUNDLE_IDENTIFIER = gvh.MeshtasticClient; PRODUCT_NAME = "$(TARGET_NAME)"; SUPPORTS_MACCATALYST = YES; @@ -2145,7 +2145,7 @@ "@executable_path/Frameworks", ); MACOSX_DEPLOYMENT_TARGET = 14.6; - MARKETING_VERSION = 2.7.7; + MARKETING_VERSION = 2.7.8; PRODUCT_BUNDLE_IDENTIFIER = gvh.MeshtasticClient; PRODUCT_NAME = "$(TARGET_NAME)"; SUPPORTS_MACCATALYST = YES; @@ -2177,7 +2177,7 @@ "@executable_path/../../Frameworks", ); MACOSX_DEPLOYMENT_TARGET = 14.6; - MARKETING_VERSION = 2.7.7; + MARKETING_VERSION = 2.7.8; PRODUCT_BUNDLE_IDENTIFIER = gvh.MeshtasticClient.Widgets; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; @@ -2210,7 +2210,7 @@ "@executable_path/../../Frameworks", ); MACOSX_DEPLOYMENT_TARGET = 14.6; - MARKETING_VERSION = 2.7.7; + MARKETING_VERSION = 2.7.8; PRODUCT_BUNDLE_IDENTIFIER = gvh.MeshtasticClient.Widgets; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = "";