diff --git a/Localizable.xcstrings b/Localizable.xcstrings index 6552af3a..2aa368ab 100644 --- a/Localizable.xcstrings +++ b/Localizable.xcstrings @@ -13742,6 +13742,9 @@ } } } + }, + "Exchange User Info" : { + }, "Exclamation" : { "localizations" : { @@ -14222,6 +14225,9 @@ } } } + }, + "Failed to exchange user info." : { + }, "Failed to get a valid position to exchange" : { "localizations" : { @@ -35632,6 +35638,9 @@ } } } + }, + "Tap to enter emoji" : { + }, "Tapback" : { "localizations" : { @@ -40315,6 +40324,12 @@ } } } + }, + "User Info Exchange Failed" : { + + }, + "User Info Sent" : { + }, "User Privacy" : { @@ -42406,7 +42421,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 0b1dec82..2e0b7faf 100644 --- a/Meshtastic.xcodeproj/project.pbxproj +++ b/Meshtastic.xcodeproj/project.pbxproj @@ -171,6 +171,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 */; }; @@ -213,7 +214,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 */; }; @@ -227,6 +227,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 */; }; @@ -518,6 +519,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 = ""; }; @@ -573,7 +575,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 = ""; }; @@ -591,6 +592,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 = ""; }; @@ -883,6 +885,7 @@ 251926882C3BAF2E00249DF5 /* Actions */ = { isa = PBXGroup; children = ( + DD4074682F1233F400BCC22F /* ExchangeUserInfoButton.swift */, DDDFE73E2D0D48FF0044463C /* IgnoreNodeButton.swift */, 251926842C3BA97800249DF5 /* FavoriteNodeButton.swift */, 251926892C3BB1B200249DF5 /* ExchangePositionsButton.swift */, @@ -1351,7 +1354,6 @@ DD1BEF492E0292220090CE24 /* KeychainHelper.swift */, DD913638270DFF4C00D7ACF3 /* LocalNotificationManager.swift */, DDA6B2E828419CF2003E8C16 /* MeshPackets.swift */, - DD964FBC296E6B01007C176F /* EmojiOnlyTextField.swift */, DD3619142B1EF9F900C41C8C /* LocationsHandler.swift */, 6D825E612C34786C008DBEE4 /* CommonRegex.swift */, 3D3417B32E2730EC006A988B /* GeoJSONOverlayManager.swift */, @@ -1432,6 +1434,7 @@ DDDB444729F8A9C900EE2349 /* String.swift */, DD77093E2AA1B146007A8BF0 /* UIColor.swift */, DDDB444F29F8AC9C00EE2349 /* UIImage.swift */, + DDA3DFD92F10B39600D8F103 /* UIKeyboardType.swift */, DDDB443F29F79AB000EE2349 /* UserDefaults.swift */, DDB75A0E2A05920E006ED576 /* FileManager.swift */, DDB75A102A059258006ED576 /* Url.swift */, @@ -1730,7 +1733,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 */, @@ -1783,6 +1785,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 */, @@ -1863,6 +1866,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 */, @@ -2177,7 +2181,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; @@ -2212,7 +2216,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; @@ -2244,7 +2248,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 = ""; @@ -2277,7 +2281,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 = ""; 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 266b6945..23762b6d 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 cd1d2961..97f005c3 100644 --- a/Meshtastic/Accessory/Accessory Manager/AccessoryManager+ToRadio.swift +++ b/Meshtastic/Accessory/Accessory Manager/AccessoryManager+ToRadio.swift @@ -2118,4 +2118,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/Helpers/EmojiOnlyTextField.swift b/Meshtastic/Helpers/EmojiOnlyTextField.swift index 4928da73..aae9e3a3 100644 --- a/Meshtastic/Helpers/EmojiOnlyTextField.swift +++ b/Meshtastic/Helpers/EmojiOnlyTextField.swift @@ -39,7 +39,7 @@ struct EmojiOnlyTextField: UIViewRepresentable { @Binding var text: String var placeholder: String = "" var onBecomeFirstResponder: (() -> Void)? - var onKeyboardTypeChanged: ((Bool) -> Void)? // true if emoji, false otherwise + 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 { diff --git a/Meshtastic/Views/Messages/MessageContextMenuItems.swift b/Meshtastic/Views/Messages/MessageContextMenuItems.swift index 14d5b3f7..0d8843ef 100644 --- a/Meshtastic/Views/Messages/MessageContextMenuItems.swift +++ b/Meshtastic/Views/Messages/MessageContextMenuItems.swift @@ -31,7 +31,10 @@ struct MessageContextMenuItems: View { } Button("Tapback") { - isShowingTapbackInput = true + // The context menu needs a moment to dismiss before the focus state can be changed. + DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { + isShowingTapbackInput = true + } } Button(action: onReply) { diff --git a/Meshtastic/Views/Messages/MessageText.swift b/Meshtastic/Views/Messages/MessageText.swift index 98734b24..fc3bb485 100644 --- a/Meshtastic/Views/Messages/MessageText.swift +++ b/Meshtastic/Views/Messages/MessageText.swift @@ -30,8 +30,10 @@ struct MessageText: View { @State private var isShowingTapbackInput = false @State private var tapbackText = "" + @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")) Text(markdownText) @@ -96,35 +98,10 @@ struct MessageText: View { 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, @@ -170,17 +147,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 index 4b961295..36a1e9b0 100644 --- a/Meshtastic/Views/Messages/TapbackInputView.swift +++ b/Meshtastic/Views/Messages/TapbackInputView.swift @@ -9,40 +9,25 @@ struct TapbackInputView: View { var body: some View { NavigationView { VStack(spacing: 0) { - EmojiOnlyTextField( - text: $text, - placeholder: "Tap to enter emoji", - onBecomeFirstResponder: { - // Text field will automatically become first responder - }, - onKeyboardTypeChanged: { shouldDismiss in - // Dismiss if keyboard switched away from emoji - if shouldDismiss { - isPresented = false + 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 = "" } - }, - onKeyboardDismissed: { - // Dismiss sheet when keyboard is dismissed - isPresented = false } - ) - .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) diff --git a/Meshtastic/Views/Nodes/Helpers/Actions/ExchangeUserInfoButton.swift b/Meshtastic/Views/Nodes/Helpers/Actions/ExchangeUserInfoButton.swift new file mode 100644 index 00000000..321b1532 --- /dev/null +++ b/Meshtastic/Views/Nodes/Helpers/Actions/ExchangeUserInfoButton.swift @@ -0,0 +1,61 @@ +import CoreData +import SwiftUI +import OSLog + +struct ExchangeUserInfoButton: View { + var node: NodeInfoEntity + var connectedNode: NodeInfoEntity + + @EnvironmentObject var accessoryManager: AccessoryManager + + @State private var isPresentingUserInfoSentAlert: Bool = false + @State private var isPresentingUserInfoFailedAlert: Bool = false + + var body: some View { + Button { + Task { + if let fromUser = connectedNode.user, let toUser = node.user { + do { + _ = try await accessoryManager.exchangeUserInfo(fromUser: fromUser, toUser: toUser) + Task { @MainActor in + isPresentingUserInfoSentAlert = true + DispatchQueue.main.asyncAfter(deadline: .now() + 2.0) { + isPresentingUserInfoSentAlert = false + } + } + } catch { + Logger.mesh.warning("Failed to exchange user info") + Task { @MainActor in + isPresentingUserInfoFailedAlert = true + DispatchQueue.main.asyncAfter(deadline: .now() + 2.0) { + isPresentingUserInfoFailedAlert = false + } + } + } + } + } + + } label: { + Label { + Text("Exchange User Info") + } icon: { + Image(systemName: "person.2.badge.gearshape") + .symbolRenderingMode(.hierarchical) + } + }.alert( + "User Info Sent", + isPresented: $isPresentingUserInfoSentAlert + ) { + Button("OK") { }.keyboardShortcut(.defaultAction) + } message: { + Text("Your user info has been sent with a request for a response with their user info.") + }.alert( + "User Info Exchange Failed", + isPresented: $isPresentingUserInfoFailedAlert + ) { + Button("OK") { }.keyboardShortcut(.defaultAction) + } message: { + Text("Failed to exchange user info.") + } + } +} diff --git a/Meshtastic/Views/Nodes/Helpers/Map/WaypointForm.swift b/Meshtastic/Views/Nodes/Helpers/Map/WaypointForm.swift index ca3c9d0f..b441bfb4 100644 --- a/Meshtastic/Views/Nodes/Helpers/Map/WaypointForm.swift +++ b/Meshtastic/Views/Nodes/Helpers/Map/WaypointForm.swift @@ -29,7 +29,6 @@ struct WaypointForm: View { @State private var expire: Date = Date.now.addingTimeInterval(60 * 480) // 1 minute * 480 = 8 Hours @State private var locked: Bool = false @State private var lockedTo: Int64 = 0 - @State private var detents: Set = [.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/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