From ad25342d884e8671456ef1cd49f6feee4e41ffad Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Fri, 17 Oct 2025 18:11:37 -0700 Subject: [PATCH 01/68] 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 63f334e5..7a4a216f 100644 --- a/Meshtastic.xcodeproj/project.pbxproj +++ b/Meshtastic.xcodeproj/project.pbxproj @@ -2094,7 +2094,7 @@ "@executable_path/Frameworks", ); MACOSX_DEPLOYMENT_TARGET = 14.6; - MARKETING_VERSION = 2.7.5; + MARKETING_VERSION = 2.7.6; PRODUCT_BUNDLE_IDENTIFIER = gvh.MeshtasticClient; PRODUCT_NAME = "$(TARGET_NAME)"; SUPPORTS_MACCATALYST = YES; @@ -2129,7 +2129,7 @@ "@executable_path/Frameworks", ); MACOSX_DEPLOYMENT_TARGET = 14.6; - MARKETING_VERSION = 2.7.5; + MARKETING_VERSION = 2.7.6; PRODUCT_BUNDLE_IDENTIFIER = gvh.MeshtasticClient; PRODUCT_NAME = "$(TARGET_NAME)"; SUPPORTS_MACCATALYST = YES; @@ -2161,7 +2161,7 @@ "@executable_path/../../Frameworks", ); MACOSX_DEPLOYMENT_TARGET = 14.6; - MARKETING_VERSION = 2.7.5; + MARKETING_VERSION = 2.7.6; PRODUCT_BUNDLE_IDENTIFIER = gvh.MeshtasticClient.Widgets; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; @@ -2194,7 +2194,7 @@ "@executable_path/../../Frameworks", ); MACOSX_DEPLOYMENT_TARGET = 14.6; - MARKETING_VERSION = 2.7.5; + MARKETING_VERSION = 2.7.6; PRODUCT_BUNDLE_IDENTIFIER = gvh.MeshtasticClient.Widgets; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; From 69c318a9e18ea7eec8e8ebc1e65cef767e62b326 Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Fri, 17 Oct 2025 18:16:00 -0700 Subject: [PATCH 02/68] Message list performance fixes into 2.7.6 (#1475) * Remove extra want config call when adding a contact * App badge and unnecessary notification fixes (#1455) * - Fix for app badge not going to zero if a message arrives while you have that chat open - Fix for push notifications popping up when a message is received while that chat is open * Fix for cancelling notifications, works now. And scroll to bottom of conversation upon new message * Fix: Channels help grammer fix (#1452) * remove outdated TCP not available on Apple devices (#1450) * Update initial onboarding view * remove toggle gating for mac * Update crash reporting opt out in real time * Update onboarding text * Use mDNS text records for node name * TCP IP and port on the connection screen * Hide app icon chooser on mac * Infinite loop hang bugfixes and performance improvements for both `UserMessageList` and `ChannelMessageList` (#1465) * 2.7.5 Working Changes (#1460) * Remove extra want config call when adding a contact * App badge and unnecessary notification fixes (#1455) * - Fix for app badge not going to zero if a message arrives while you have that chat open - Fix for push notifications popping up when a message is received while that chat is open * Fix for cancelling notifications, works now. And scroll to bottom of conversation upon new message * Fix: Channels help grammer fix (#1452) * remove outdated TCP not available on Apple devices (#1450) * Update initial onboarding view * remove toggle gating for mac * Update crash reporting opt out in real time * Update onboarding text --------- Co-authored-by: Gnome Adrift <646322+gnomeadrift@users.noreply.github.com> Co-authored-by: Zain Kergaye <62440012+ZainKergaye@users.noreply.github.com> Co-authored-by: NillRudd <102033730+NillRudd@users.noreply.github.com> * UserEntity: add mostRecentMessage and unreadMessages with early exit when lastMessage is nil, and fetch 1 row (not N) otherwise * UserList: replace 5 slow calls to user.messageList with new fast calls * NodeList: always put the connected node at the top of list (if it matches the node filters) * ChannelEntity: add faster mostRecentPrivateMessage and unreadMessages which fetch 1 row (not N) * ChannelList: replace 5 calls to channel.allPrivateMessage with new fast calls * Fix incorrect appState.unreadDirectMessages calculations * MyInfoEntity: also fix unreadMessages count here to be fast, and use it for appState.unreadChannelMessages * UserMessageList: use @FetchRequest to prevent the N^2 behavior that was happening in calls to allPrivateMessages * Refactor ChannelEntityExtension and MyInfoEntityExtension to be more similar to UserEntityExtension * Remove SwiftUI-infinite-loop-causing `.id(redrawTapbacksTrigger)` in ChannelMessageList and UserMessageList (duplicate row ids) * MyInfoEntityExtension: exclude emoji tapbacks (which never get marked as read anyway) from unread message count * Add SaveChannelLinkData so MessageText and MeshtasticApp can use .sheet(item: ...) and avoid infinite loop hang due to Binding rebuild * ChannelMessageList and UserMessageList: switch to stable messageId for ForEach SwiftUI row identity * ChannelMessageList and UserMessageList: debouncedScrollToBottom; keyboardWillShowNotification/keyboardDidShowNotification * ChannelMessageList and UserMessageList: scroll to bottom onFirstAppear * ChannelMessageList and UserMessageList: block spurious markMessagesAsRead when this View is not active --------- Co-authored-by: Garth Vander Houwen Co-authored-by: Gnome Adrift <646322+gnomeadrift@users.noreply.github.com> Co-authored-by: Zain Kergaye <62440012+ZainKergaye@users.noreply.github.com> Co-authored-by: NillRudd <102033730+NillRudd@users.noreply.github.com> * message-list-performance: revert scrolling changes (#1472) * Revert e0f0b4a0f749d2e83946f2c1297e5c97c9fdf46e (ChannelMessageList and UserMessageList: scroll to bottom onFirstAppear) * Revert "ChannelMessageList and UserMessageList: debouncedScrollToBottom; keyboardWillShowNotification/keyboardDidShowNotification" This reverts commit ee1a7c44157eb6970ec2111fc1ac4d67a44a8238. --------- Co-authored-by: Gnome Adrift <646322+gnomeadrift@users.noreply.github.com> Co-authored-by: Zain Kergaye <62440012+ZainKergaye@users.noreply.github.com> Co-authored-by: NillRudd <102033730+NillRudd@users.noreply.github.com> Co-authored-by: Jake-B Co-authored-by: Mike Robbins --- Localizable.xcstrings | 7 +++ .../AccessoryManager+MQTT.swift | 4 +- Meshtastic/Accessory/Protocols/Device.swift | 6 ++- .../Transports/TCP/TCPTransport.swift | 21 +++++++- .../CoreData/ChannelEntityExtension.swift | 36 ++++++++++--- .../CoreData/MyInfoEntityExtension.swift | 30 ++++++++--- .../CoreData/UserEntityExtension.swift | 44 ++++++++++++--- Meshtastic/Helpers/MeshPackets.swift | 7 ++- Meshtastic/MeshtasticApp.swift | 53 ++++++++---------- Meshtastic/Views/Connect/Connect.swift | 27 ++++++++-- Meshtastic/Views/Messages/ChannelList.swift | 13 +++-- .../Views/Messages/ChannelMessageList.swift | 26 +++++++-- Meshtastic/Views/Messages/MessageText.swift | 33 ++++-------- Meshtastic/Views/Messages/UserList.swift | 26 ++++----- .../Views/Messages/UserMessageList.swift | 54 ++++++++++++++----- .../Nodes/Helpers/ShareContactQRDialog.swift | 2 + Meshtastic/Views/Nodes/NodeList.swift | 40 +++++++------- .../Views/Onboarding/DeviceOnboarding.swift | 2 +- Meshtastic/Views/Settings/AppSettings.swift | 4 ++ .../Views/Settings/SaveChannelQRCode.swift | 6 +++ 20 files changed, 302 insertions(+), 139 deletions(-) diff --git a/Localizable.xcstrings b/Localizable.xcstrings index 9bbbd0bd..4e78c69c 100644 --- a/Localizable.xcstrings +++ b/Localizable.xcstrings @@ -20930,6 +20930,10 @@ } } }, + "Meshtastic does not collect any personal information. We do anonymously collect usage and crash data to improve the app. You can opt out under app settings." : { + "comment" : "A description of Meshtastic's data collection practices.", + "isCommentAutoGenerated" : true + }, "Meshtastic Node %@ has shared channels with you" : { "localizations" : { "de" : { @@ -40176,6 +40180,9 @@ } } } + }, + "User Privacy" : { + }, "User Uploaded" : { "comment" : "Data source label for user uploaded files", diff --git a/Meshtastic/Accessory/Accessory Manager/AccessoryManager+MQTT.swift b/Meshtastic/Accessory/Accessory Manager/AccessoryManager+MQTT.swift index 7c286336..0a246f88 100644 --- a/Meshtastic/Accessory/Accessory Manager/AccessoryManager+MQTT.swift +++ b/Meshtastic/Accessory/Accessory Manager/AccessoryManager+MQTT.swift @@ -32,8 +32,8 @@ extension AccessoryManager { } } // Set initial unread message badge states - appState.unreadChannelMessages = fetchedNodeInfo[0].myInfo?.unreadMessages ?? 0 - appState.unreadDirectMessages = fetchedNodeInfo[0].user?.unreadMessages ?? 0 + appState.unreadChannelMessages = fetchedNodeInfo[0].myInfo?.unreadMessages(context: context) ?? 0 + appState.unreadDirectMessages = fetchedNodeInfo[0].user?.unreadMessages(context: context, skipLastMessageCheck: true) ?? 0 // skipLastMessageCheck=true because we don't update lastMessage on our own connected node } if fetchedNodeInfo.count == 1 && fetchedNodeInfo[0].rangeTestConfig?.enabled == true { wantRangeTestPackets = true diff --git a/Meshtastic/Accessory/Protocols/Device.swift b/Meshtastic/Accessory/Protocols/Device.swift index 03ae1d1d..844eff31 100644 --- a/Meshtastic/Accessory/Protocols/Device.swift +++ b/Meshtastic/Accessory/Protocols/Device.swift @@ -23,7 +23,10 @@ struct Device: Identifiable, Hashable { var connectionState: ConnectionState var wasRestored: Bool = false - init(id: UUID, name: String, transportType: TransportType, identifier: String, connectionState: ConnectionState = .disconnected, rssi: Int? = nil, num: Int64? = nil, wasRestored: Bool = false) { + + var connectionDetails: String? + + init(id: UUID, name: String, transportType: TransportType, identifier: String, connectionState: ConnectionState = .disconnected, rssi: Int? = nil, num: Int64? = nil, connectionDetails: String? = nil, wasRestored: Bool = false) { self.id = id self.name = name self.transportType = transportType @@ -32,6 +35,7 @@ struct Device: Identifiable, Hashable { self.rssi = rssi self.num = num self.wasRestored = wasRestored + self.connectionDetails = connectionDetails } var rssiString: String { diff --git a/Meshtastic/Accessory/Transports/TCP/TCPTransport.swift b/Meshtastic/Accessory/Transports/TCP/TCPTransport.swift index d753fc76..787d257c 100644 --- a/Meshtastic/Accessory/Transports/TCP/TCPTransport.swift +++ b/Meshtastic/Accessory/Transports/TCP/TCPTransport.swift @@ -78,10 +78,27 @@ class TCPTransport: NSObject, Transport, NetServiceBrowserDelegate, NetServiceDe // Save the resolved service locally for later services[service.name] = ResolvedService(id: idString, service: service, host: host, port: port) + let name: String + if let txtRecords = service.txtRecordData().map({NetService.dictionary(fromTXTRecord: $0)}) { + var nodeNameString = "" + if let shortNameData = txtRecords["shortname"] { + nodeNameString += String(decoding: shortNameData, as: UTF8.self) + } + if let nodeId = txtRecords["id"], nodeId.count > 4 { + if nodeNameString.count > 0 { + nodeNameString += "_" + } + nodeNameString += String(decoding: nodeId.suffix(4), as: UTF8.self) + } + name = nodeNameString + } else { + name = "\(service.name) (\(ip))" + } let device = Device(id: idString, - name: "\(service.name) (\(ip))", + name: name, transportType: .tcp, - identifier: "\(host):\(port)") + identifier: "\(host):\(port)", + connectionDetails: "\(ip):\(port)") continuation?.yield(.deviceFound(device)) } diff --git a/Meshtastic/Extensions/CoreData/ChannelEntityExtension.swift b/Meshtastic/Extensions/CoreData/ChannelEntityExtension.swift index c85eef4a..8d7962b4 100644 --- a/Meshtastic/Extensions/CoreData/ChannelEntityExtension.swift +++ b/Meshtastic/Extensions/CoreData/ChannelEntityExtension.swift @@ -9,22 +9,46 @@ import CoreData import MeshtasticProtobufs extension ChannelEntity { + var messagePredicate: NSPredicate { + return NSPredicate(format: "channel == %ld AND toUser == nil AND isEmoji == false", self.index) + } + + var messageFetchRequest: NSFetchRequest { + let fetchRequest = MessageEntity.fetchRequest() + fetchRequest.sortDescriptors = [NSSortDescriptor(key: "messageTimestamp", ascending: true)] + fetchRequest.predicate = messagePredicate + return fetchRequest + } var allPrivateMessages: [MessageEntity] { let context = PersistenceController.shared.container.viewContext - let fetchRequest = MessageEntity.fetchRequest() - fetchRequest.sortDescriptors = [NSSortDescriptor(key: "messageTimestamp", ascending: true)] - fetchRequest.predicate = NSPredicate(format: "channel == %ld AND toUser == nil AND isEmoji == false", self.index) + let fetchRequest = messageFetchRequest return (try? context.fetch(fetchRequest)) ?? [MessageEntity]() } - var unreadMessages: Int { + var mostRecentPrivateMessage: MessageEntity? { + // Most recent channel message (descending, limit 1) + let context = PersistenceController.shared.container.viewContext + let fetchRequest = messageFetchRequest + fetchRequest.sortDescriptors = [NSSortDescriptor(key: "messageTimestamp", ascending: false)] + fetchRequest.fetchLimit = 1 - let unreadMessages = allPrivateMessages.filter { ($0 as AnyObject).read == false } - return unreadMessages.count + return (try? context.fetch(fetchRequest))?.first } + func unreadMessages(context: NSManagedObjectContext) -> Int { + let context = PersistenceController.shared.container.viewContext + let fetchRequest = messageFetchRequest + fetchRequest.sortDescriptors = [] // sort is irrelvant. + fetchRequest.predicate = NSCompoundPredicate(andPredicateWithSubpredicates: [fetchRequest.predicate!, NSPredicate(format: "read == false")]) + + return (try? context.count(for: fetchRequest)) ?? 0 + } + + // Backwards-compatible property (uses viewContext) + var unreadMessages: Int { unreadMessages(context: PersistenceController.shared.container.viewContext) } + var protoBuf: Channel { var channel = Channel() channel.index = self.index diff --git a/Meshtastic/Extensions/CoreData/MyInfoEntityExtension.swift b/Meshtastic/Extensions/CoreData/MyInfoEntityExtension.swift index 68a48ee1..136b64cb 100644 --- a/Meshtastic/Extensions/CoreData/MyInfoEntityExtension.swift +++ b/Meshtastic/Extensions/CoreData/MyInfoEntityExtension.swift @@ -6,23 +6,39 @@ // import Foundation +import CoreData extension MyInfoEntity { + var messagePredicate: NSPredicate { + return NSPredicate(format: "toUser == nil AND isEmoji == false") + } + + var messageFetchRequest: NSFetchRequest { + let fetchRequest = MessageEntity.fetchRequest() + fetchRequest.sortDescriptors = [NSSortDescriptor(key: "messageTimestamp", ascending: true)] + fetchRequest.predicate = messagePredicate + return fetchRequest + } var messageList: [MessageEntity] { let context = PersistenceController.shared.container.viewContext - let fetchRequest = MessageEntity.fetchRequest() - fetchRequest.sortDescriptors = [NSSortDescriptor(key: "messageTimestamp", ascending: true)] - fetchRequest.predicate = NSPredicate(format: "toUser == nil") + let fetchRequest = messageFetchRequest - return (try? context.fetch(fetchRequest)) ?? [MessageEntity]() + return (try? context.fetch(messageFetchRequest)) ?? [MessageEntity]() } - var unreadMessages: Int { - let unreadMessages = messageList.filter { ($0 as AnyObject).read == false && ($0 as AnyObject).isEmoji == false } - return unreadMessages.count + func unreadMessages(context: NSManagedObjectContext) -> Int { + // Returns the count of unread *channel* messages + let fetchRequest = messageFetchRequest + fetchRequest.sortDescriptors = [] // sort is irrelvant. + fetchRequest.predicate = NSCompoundPredicate(andPredicateWithSubpredicates: [fetchRequest.predicate!, NSPredicate(format: "read == false")]) + + return (try? context.count(for: fetchRequest)) ?? 0 } + // Backwards-compatible property (uses viewContext) + var unreadMessages: Int { unreadMessages(context: PersistenceController.shared.container.viewContext) } + var hasAdmin: Bool { let adminChannel = channels?.filter { ($0 as AnyObject).name?.lowercased() == "admin" } return adminChannel?.count ?? 0 > 0 diff --git a/Meshtastic/Extensions/CoreData/UserEntityExtension.swift b/Meshtastic/Extensions/CoreData/UserEntityExtension.swift index 2a87ed9f..7774ba80 100644 --- a/Meshtastic/Extensions/CoreData/UserEntityExtension.swift +++ b/Meshtastic/Extensions/CoreData/UserEntityExtension.swift @@ -10,16 +10,37 @@ import CoreData import MeshtasticProtobufs extension UserEntity { + var messagePredicate: NSPredicate { + return NSPredicate(format: "((toUser == %@) OR (fromUser == %@)) AND toUser != nil AND fromUser != nil AND isEmoji == false AND admin = false AND portNum != 10", self, self) + } + + var messageFetchRequest: NSFetchRequest { + let fetchRequest = MessageEntity.fetchRequest() + fetchRequest.sortDescriptors = [NSSortDescriptor(key: "messageTimestamp", ascending: true)] + fetchRequest.predicate = messagePredicate + return fetchRequest + } var messageList: [MessageEntity] { let context = PersistenceController.shared.container.viewContext - let fetchRequest = MessageEntity.fetchRequest() - fetchRequest.sortDescriptors = [NSSortDescriptor(key: "messageTimestamp", ascending: true)] - fetchRequest.predicate = NSPredicate(format: "((toUser == %@) OR (fromUser == %@)) AND toUser != nil AND fromUser != nil AND isEmoji == false AND admin = false AND portNum != 10", self, self) + let fetchRequest = messageFetchRequest return (try? context.fetch(fetchRequest)) ?? [MessageEntity]() } + var mostRecentMessage: MessageEntity? { + // Most contacts will have no DMs history, so we can return early. + guard self.lastMessage != nil else { return nil; } + + // Most recent DM for this user (descending, limit 1) + let context = PersistenceController.shared.container.viewContext + let fetchRequest = messageFetchRequest + fetchRequest.sortDescriptors = [NSSortDescriptor(key: "messageTimestamp", ascending: false)] + fetchRequest.fetchLimit = 1 + + return (try? context.fetch(fetchRequest))?.first + } + var sensorMessageList: [MessageEntity] { let context = PersistenceController.shared.container.viewContext let fetchRequest = MessageEntity.fetchRequest() @@ -29,10 +50,21 @@ extension UserEntity { return (try? context.fetch(fetchRequest)) ?? [MessageEntity]() } - var unreadMessages: Int { - let unreadMessages = messageList.filter { ($0 as AnyObject).read == false && ($0 as AnyObject).isEmoji == false } - return unreadMessages.count + func unreadMessages(context: NSManagedObjectContext, skipLastMessageCheck: Bool = false) -> Int { + // Most contacts will have no DMs history, so we can return early. + // (For our own node, set skipLastMessageCheck=true, because we don't update lastMessage on our own connected node.) + guard self.lastMessage != nil || skipLastMessageCheck else { return 0; } + + let fetchRequest = messageFetchRequest + fetchRequest.sortDescriptors = [] // sort is irrelvant. + fetchRequest.predicate = NSCompoundPredicate(andPredicateWithSubpredicates: [fetchRequest.predicate!, NSPredicate(format: "read == false")]) + + return (try? context.count(for: fetchRequest)) ?? 0 } + + // Backwards-compatible property (uses viewContext) + var unreadMessages: Int { unreadMessages(context: PersistenceController.shared.container.viewContext) } + /// SVG Images for Vendors who are signed project backers var hardwareImage: String? { guard let hwModel else { return nil } diff --git a/Meshtastic/Helpers/MeshPackets.swift b/Meshtastic/Helpers/MeshPackets.swift index 4d217dc7..caffd8b4 100644 --- a/Meshtastic/Helpers/MeshPackets.swift +++ b/Meshtastic/Helpers/MeshPackets.swift @@ -1040,7 +1040,10 @@ func textMessageAppPacket( if newMessage.fromUser != nil && newMessage.toUser != nil { // Set Unread Message Indicators if packet.to == connectedNode { - appState?.unreadDirectMessages = newMessage.toUser?.unreadMessages ?? 0 + let unreadCount = newMessage.toUser?.unreadMessages(context: context, skipLastMessageCheck: true) ?? 0 // skipLastMessageCheck=true because we don't update lastMessage on our own connected node + Task { @MainActor in + appState?.unreadDirectMessages = unreadCount + } } if !(newMessage.fromUser?.mute ?? false) && newMessage.isEmoji == false { // Create an iOS Notification for the received DM message @@ -1068,7 +1071,7 @@ func textMessageAppPacket( do { let fetchedMyInfo = try context.fetch(fetchMyInfoRequest) if !fetchedMyInfo.isEmpty { - appState?.unreadChannelMessages = fetchedMyInfo[0].unreadMessages + appState?.unreadChannelMessages = fetchedMyInfo[0].unreadMessages(context: context) for channel in (fetchedMyInfo[0].channels?.array ?? []) as? [ChannelEntity] ?? [] { if channel.index == newMessage.channel { context.refresh(channel, mergeChanges: true) diff --git a/Meshtastic/MeshtasticApp.swift b/Meshtastic/MeshtasticApp.swift index a83ddb9b..fccf9a7c 100644 --- a/Meshtastic/MeshtasticApp.swift +++ b/Meshtastic/MeshtasticApp.swift @@ -19,10 +19,8 @@ struct MeshtasticAppleApp: App { private let persistenceController: PersistenceController private let accessoryManager: AccessoryManager @Environment(\.scenePhase) var scenePhase - @State var saveChannels = false + @State var saveChannelLink: SaveChannelLinkData? @State var incomingUrl: URL? - @State var channelSettings: String? - @State var addChannels = false init() { @@ -36,7 +34,7 @@ struct MeshtasticAppleApp: App { let appID = "79fe92a9-74c9-4c8f-ba63-6308384ecfa9" let clientToken = "pub4427bea20dbdb08a6af68034de22cd3b" var environment = "AppStore" - + #if DEBUG environment = "Local" #else @@ -44,7 +42,8 @@ struct MeshtasticAppleApp: App { environment = "TestFlight" } #endif - + +#if false Datadog.initialize( with: Datadog.Configuration( clientToken: clientToken, @@ -81,6 +80,7 @@ struct MeshtasticAppleApp: App { ) ) } +#endif accessoryManager = AccessoryManager.shared accessoryManager.appState = appState @@ -110,20 +110,11 @@ struct MeshtasticAppleApp: App { appState: appState, router: appState.router ) - .sheet(isPresented: Binding( - get: { - saveChannels && !(channelSettings == nil) - }, - set: { newValue in - saveChannels = newValue - if !newValue { - channelSettings = nil - } - } - )) { + .sheet(item: $saveChannelLink + ) { link in SaveChannelQRCode( - channelSetLink: channelSettings ?? "Empty Channel URL", - addChannels: addChannels, + channelSetLink: link.data, + addChannels: link.add, accessoryManager: accessoryManager ) .presentationDetents([.large]) .presentationDragIndicator(.visible) @@ -131,54 +122,54 @@ struct MeshtasticAppleApp: App { .onContinueUserActivity(NSUserActivityTypeBrowsingWeb) { userActivity in Logger.mesh.debug("URL received \(userActivity, privacy: .public)") self.incomingUrl = userActivity.webpageURL - self.saveChannels = false + self.saveChannelLink = nil + var addChannels = false if self.incomingUrl?.absoluteString.lowercased().contains("meshtastic.org/v/#") == true { ContactURLHandler.handleContactUrl(url: self.incomingUrl!, accessoryManager: accessoryManager) } else if self.incomingUrl?.absoluteString.lowercased().contains("meshtastic.org/e/") == true { if let components = self.incomingUrl?.absoluteString.components(separatedBy: "#") { - self.addChannels = Bool(self.incomingUrl?["add"] ?? "false") ?? false + addChannels = Bool(self.incomingUrl?["add"] ?? "false") ?? false if (self.incomingUrl?.absoluteString.lowercased().contains("?")) != nil { guard let cs = components.last!.components(separatedBy: "?").first else { return } - self.channelSettings = cs + self.saveChannelLink = SaveChannelLinkData(data: cs, add: addChannels) } else { guard let cs = components.first else { return } - self.channelSettings = cs + self.saveChannelLink = SaveChannelLinkData(data: cs, add: addChannels) } - Logger.services.debug("Add Channel \(self.addChannels, privacy: .public)") + Logger.services.debug("Add Channel \(addChannels, privacy: .public)") } - self.saveChannels = true Logger.mesh.debug("User wants to open a Channel Settings URL: \(self.incomingUrl?.absoluteString ?? "No QR Code Link")") } - if self.saveChannels { + if self.saveChannelLink != nil { Logger.mesh.debug("User wants to open Channel Settings URL: \(String(describing: self.incomingUrl!.relativeString), privacy: .public)") } } .onOpenURL(perform: { (url) in Logger.mesh.debug("Some sort of URL was received \(url, privacy: .public)") self.incomingUrl = url + var addChannels = false if url.absoluteString.lowercased().contains("meshtastic.org/v/#") { ContactURLHandler.handleContactUrl(url: url, accessoryManager: accessoryManager) } else if url.absoluteString.lowercased().contains("meshtastic.org/e/") { if let components = self.incomingUrl?.absoluteString.components(separatedBy: "#") { - self.addChannels = Bool(self.incomingUrl?["add"] ?? "false") ?? false + addChannels = Bool(self.incomingUrl?["add"] ?? "false") ?? false if self.incomingUrl?.absoluteString.lowercased().contains("?") != nil { guard let cs = components.last!.components(separatedBy: "?").first else { return } - self.channelSettings = cs + self.saveChannelLink = SaveChannelLinkData(data: cs, add: addChannels) } else { guard let cs = components.first else { return } - self.channelSettings = cs + self.saveChannelLink = SaveChannelLinkData(data: cs, add: addChannels) } - Logger.services.debug("Add Channel \(self.addChannels, privacy: .public)") + Logger.services.debug("Add Channel \(addChannels, privacy: .public)") } - self.saveChannels = true Logger.mesh.debug("User wants to open a Channel Settings URL: \(self.incomingUrl?.absoluteString ?? "No QR Code Link", privacy: .public)") } else if url.absoluteString.lowercased().contains("meshtastic:///") { appState.router.route(url: url) @@ -221,7 +212,7 @@ struct MeshtasticAppleApp: App { .environment(\.managedObjectContext, persistenceController.container.viewContext) .environmentObject(appState) .environmentObject(accessoryManager) - .environmentObject(appState.router) + .environmentObject(appState.router) } } diff --git a/Meshtastic/Views/Connect/Connect.swift b/Meshtastic/Views/Connect/Connect.swift index 8915d9ff..7a264aa6 100644 --- a/Meshtastic/Views/Connect/Connect.swift +++ b/Meshtastic/Views/Connect/Connect.swift @@ -64,10 +64,22 @@ struct Connect: View { } Text("Connection Name").font(.callout)+Text(": \(connectedDevice.name.addingVariationSelectors)") .font(.callout).foregroundColor(Color.gray) - HStack(alignment: .firstTextBaseline) { - TransportIcon(transportType: connectedDevice.transportType) + HStack { if connectedDevice.transportType == .ble { - connectedDevice.getSignalStrength().map { SignalStrengthIndicator(signalStrength: $0, width: 5, height: 20) } + // baseline aligned looks better for the signal meter + HStack(alignment: .firstTextBaseline) { + TransportIcon(transportType: connectedDevice.transportType) + connectedDevice.getSignalStrength().map { SignalStrengthIndicator(signalStrength: $0, width: 5, height: 20) } + } + } else if connectedDevice.transportType == .tcp { + // Not baseline aligned looks better for the connection string + HStack { + TransportIcon(transportType: connectedDevice.transportType) + Text("\(connectedDevice.connectionDetails ?? connectedDevice.identifier)") + .foregroundColor(.gray) + } + } else { + TransportIcon(transportType: connectedDevice.transportType) } Spacer() } @@ -297,7 +309,14 @@ struct Connect: View { Text(device.name).font(.callout) } // Show transport type - TransportIcon(transportType: device.transportType) + HStack { + TransportIcon(transportType: device.transportType) + if device.transportType == .tcp { + // Show IP and Port + Text("\(device.connectionDetails ?? device.identifier)") + .foregroundColor(.gray) + } + } } Spacer() VStack { diff --git a/Meshtastic/Views/Messages/ChannelList.swift b/Meshtastic/Views/Messages/ChannelList.swift index 5b3af8f6..16660203 100644 --- a/Meshtastic/Views/Messages/ChannelList.swift +++ b/Meshtastic/Views/Messages/ChannelList.swift @@ -37,14 +37,16 @@ struct ChannelList: View { let dateFormatString = (localeDateFormat ?? "MM/dd/YY") NavigationLink(value: channel) { - let mostRecent = channel.allPrivateMessages.last(where: { $0.channel == channel.index }) + let mostRecent = channel.mostRecentPrivateMessage + let hasMessages = mostRecent != nil + let hasUnreadMessages = hasMessages && (channel.unreadMessages > 0) let lastMessageTime = Date(timeIntervalSince1970: TimeInterval(Int64((mostRecent?.messageTimestamp ?? 0 )))) let lastMessageDay = Calendar.current.dateComponents([.day], from: lastMessageTime).day ?? 0 let currentDay = Calendar.current.dateComponents([.day], from: Date()).day ?? 0 ZStack { Image(systemName: "circle.fill") - .opacity(channel.unreadMessages > 0 ? 1 : 0) + .opacity(hasUnreadMessages ? 1 : 0) .font(.system(size: 10)) .foregroundColor(.accentColor) .brightness(0.2) @@ -70,7 +72,7 @@ struct ChannelList: View { Spacer() - if channel.allPrivateMessages.count > 0 { + if hasMessages { if lastMessageDay == currentDay { Text(lastMessageTime, style: .time ) @@ -95,7 +97,7 @@ struct ChannelList: View { } } - if channel.allPrivateMessages.count > 0 { + if hasMessages { HStack(alignment: .top) { Text("\(mostRecent != nil ? mostRecent!.messagePayload! : " ")") // .font(.system(size: 16)) @@ -112,6 +114,7 @@ struct ChannelList: View { if let node, let myInfo = node.myInfo { List(selection: $channelSelection) { ForEach(channels) { (channel: ChannelEntity) in + let hasMessages = channel.mostRecentPrivateMessage != nil if !restrictedChannels.contains(channel.name?.lowercased() ?? "") { makeChannelRow(myInfo: myInfo, channel: channel) .alignmentGuide(.listRowSeparatorLeading) { @@ -119,7 +122,7 @@ struct ChannelList: View { } .frame(height: 62) .contextMenu { - if channel.allPrivateMessages.count > 0 { + if hasMessages { Button(role: .destructive) { isPresentingDeleteChannelMessagesConfirm = true channelToDeleteMessages = channel diff --git a/Meshtastic/Views/Messages/ChannelMessageList.swift b/Meshtastic/Views/Messages/ChannelMessageList.swift index 857028f1..ba0bc701 100644 --- a/Meshtastic/Views/Messages/ChannelMessageList.swift +++ b/Meshtastic/Views/Messages/ChannelMessageList.swift @@ -13,6 +13,7 @@ import SwiftUI struct ChannelMessageList: View { @EnvironmentObject var appState: AppState @EnvironmentObject var router: Router + @Environment(\.scenePhase) var scenePhase @Environment(\.managedObjectContext) var context @EnvironmentObject var accessoryManager: AccessoryManager @FocusState var messageFieldFocused: Bool @@ -58,14 +59,29 @@ struct ChannelMessageList: View { Logger.data.error("Failed to read messages: \(error.localizedDescription, privacy: .public)") } } - + + private func routerIsShowingThisChannel() -> Bool { + guard router.navigationState.selectedTab == .messages else { return false } + return scenePhase == .active + } + var body: some View { + // Cast allPrivateMessages to an array for easier indexing and ForEach. + let messages: [MessageEntity] = Array(allPrivateMessages) + + // Precompute previous message + let previousByID: [Int64: MessageEntity?] = { + var dict = [Int64: MessageEntity?]() + var prev: MessageEntity? + for m in messages { dict[m.messageId] = prev; prev = m } + return dict + }() + ScrollViewReader { scrollView in ScrollView { LazyVStack { - ForEach(allPrivateMessages.indices, id: \.self) { index in - let message = allPrivateMessages[index] - let previousMessage = index > 0 ? allPrivateMessages[index - 1] : nil + ForEach(messages, id: \.messageId) { message in + let previousMessage: MessageEntity? = previousByID[message.messageId] ?? nil ChannelMessageRow( message: message, @@ -92,7 +108,7 @@ struct ChannelMessageList: View { } } } - .id(redrawTapbacksTrigger) + } Color.clear .frame(height: 1) diff --git a/Meshtastic/Views/Messages/MessageText.swift b/Meshtastic/Views/Messages/MessageText.swift index 19d0c715..28df8fba 100644 --- a/Meshtastic/Views/Messages/MessageText.swift +++ b/Meshtastic/Views/Messages/MessageText.swift @@ -25,9 +25,7 @@ struct MessageText: View { let isCurrentUser: Bool let onReply: () -> Void // State for handling channel URL sheet - @State private var saveChannels = false - @State private var channelSettings: String? - @State private var addChannels = false + @State private var saveChannelLink: SaveChannelLinkData? @State private var isShowingDeleteConfirmation = false var body: some View { @@ -97,7 +95,8 @@ struct MessageText: View { ) } .environment(\.openURL, OpenURLAction { url in - channelSettings = nil + saveChannelLink = nil + var addChannels = false if url.absoluteString.lowercased().contains("meshtastic.org/v/#") { // Handle contact URL ContactURLHandler.handleContactUrl(url: url, accessoryManager: AccessoryManager.shared) @@ -109,35 +108,25 @@ struct MessageText: View { Logger.services.error("No valid components found in channel URL: \(url.absoluteString, privacy: .public)") return .discarded } - self.addChannels = Bool(url.query?.contains("add=true") ?? false) + 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.channelSettings = nil + self.saveChannelLink = nil return .discarded } - self.channelSettings = lastComponent.components(separatedBy: "?").first ?? "" - Logger.services.debug("Add Channel: \(self.addChannels, privacy: .public)") - self.saveChannels = true + 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 }) // Display sheet for channel settings - .sheet(isPresented: Binding( - get: { - saveChannels && !(channelSettings == nil) - }, - set: { newValue in - saveChannels = newValue - if !newValue { - channelSettings = nil - } - } - )) { + .sheet(item: $saveChannelLink) { link in SaveChannelQRCode( - channelSetLink: channelSettings ?? "Empty Channel URL", - addChannels: addChannels, + channelSetLink: link.data, + addChannels: link.add, accessoryManager: accessoryManager ) .presentationDetents([.large]) diff --git a/Meshtastic/Views/Messages/UserList.swift b/Meshtastic/Views/Messages/UserList.swift index 2199e997..16ba0b7b 100644 --- a/Meshtastic/Views/Messages/UserList.swift +++ b/Meshtastic/Views/Messages/UserList.swift @@ -11,7 +11,7 @@ import OSLog import TipKit struct UserList: View { - + @Environment(\.managedObjectContext) var context @EnvironmentObject var accessoryManager: AccessoryManager @State private var editingFilters = false @@ -20,7 +20,7 @@ struct UserList: View { @StateObject private var filters: NodeFilterParameters = NodeFilterParameters() @Binding var node: NodeInfoEntity? @Binding var userSelection: UserEntity? - + var body: some View { VStack { FilteredUserList(withFilters: filters, node: $node, userSelection: $userSelection) @@ -92,13 +92,15 @@ fileprivate struct FilteredUserList: View { self._node = node self._userSelection = userSelection } - + var body: some View { let localeDateFormat = DateFormatter.dateFormat(fromTemplate: "yyMMdd", options: 0, locale: Locale.current) let dateFormatString = (localeDateFormat ?? "MM/dd/YY") List(users, selection: $userSelection) { user in - let mostRecent = user.messageList.last + let mostRecent = user.mostRecentMessage + let hasMessages = mostRecent != nil + let hasUnreadMessages = user.unreadMessages > 0 let lastMessageTime = Date(timeIntervalSince1970: TimeInterval(Int64((mostRecent?.messageTimestamp ?? 0 )))) let lastMessageDay = Calendar.current.dateComponents([.day], from: lastMessageTime).day ?? 0 let currentDay = Calendar.current.dateComponents([.day], from: Date()).day ?? 0 @@ -106,14 +108,14 @@ fileprivate struct FilteredUserList: View { NavigationLink(value: user) { ZStack { Image(systemName: "circle.fill") - .opacity(user.unreadMessages > 0 ? 1 : 0) + .opacity(hasUnreadMessages ? 1 : 0) .font(.system(size: 10)) .foregroundColor(.accentColor) .brightness(0.2) } - + CircleText(text: user.shortName ?? "?", color: Color(UIColor(hex: UInt32(user.num)))) - + VStack(alignment: .leading) { HStack { if user.pkiEncrypted { @@ -137,7 +139,7 @@ fileprivate struct FilteredUserList: View { Image(systemName: "star.fill") .foregroundColor(.yellow) } - if user.messageList.count > 0 { + if hasMessages { if lastMessageDay == currentDay { Text(lastMessageTime, style: .time ) .font(.footnote) @@ -157,8 +159,8 @@ fileprivate struct FilteredUserList: View { } } } - - if user.messageList.count > 0 { + + if hasMessages { HStack(alignment: .top) { Text("\(mostRecent != nil ? mostRecent!.messagePayload! : " ")") .font(.footnote) @@ -207,7 +209,7 @@ fileprivate struct FilteredUserList: View { } label: { Label(user.mute ? "Show Alerts" : "Hide Alerts", systemImage: user.mute ? "bell" : "bell.slash") } - if user.messageList.count > 0 { + if hasMessages { Button(role: .destructive) { isPresentingDeleteUserMessagesConfirm = true userToDeleteMessages = user @@ -316,7 +318,7 @@ fileprivate extension NodeFilterParameters { predicates.append(isIgnoredPredicate) let isConnectedNodePredicate = NSPredicate(format: "NOT (numString CONTAINS %@)", String(UserDefaults.preferredPeripheralNum)) predicates.append(isConnectedNodePredicate) - + // Combine all predicates let finalPredicate = predicates.isEmpty ? NSPredicate(value: true) : NSCompoundPredicate(type: .and, subpredicates: predicates) return finalPredicate diff --git a/Meshtastic/Views/Messages/UserMessageList.swift b/Meshtastic/Views/Messages/UserMessageList.swift index 87afb16f..394431d5 100644 --- a/Meshtastic/Views/Messages/UserMessageList.swift +++ b/Meshtastic/Views/Messages/UserMessageList.swift @@ -11,9 +11,10 @@ import OSLog import MeshtasticProtobufs // Added to ensure RoutingError is accessible if needed struct UserMessageList: View { - @EnvironmentObject var appState: AppState + @EnvironmentObject var router: Router @EnvironmentObject var accessoryManager: AccessoryManager + @Environment(\.scenePhase) var scenePhase @Environment(\.managedObjectContext) var context @FocusState var messageFieldFocused: Bool @ObservedObject var user: UserEntity @@ -21,17 +22,21 @@ struct UserMessageList: View { @State private var messageToHighlight: Int64 = 0 @State private var redrawTapbacksTrigger = UUID() @AppStorage("preferredPeripheralNum") private var preferredPeripheralNum = -1 - - private var allPrivateMessages: [MessageEntity] { - // Cast user.messageList to an array for easier indexing and ForEach. - return user.messageList.compactMap { $0 as MessageEntity } + @FetchRequest private var allPrivateMessages: FetchedResults + + init(user: UserEntity) { + self.user = user + + // Configure fetch request here + let request: NSFetchRequest = user.messageFetchRequest + _allPrivateMessages = FetchRequest(fetchRequest: request) } - + func handleInteractionComplete() { markMessagesAsRead() redrawTapbacksTrigger = UUID() } - + func markMessagesAsRead() { do { for unreadMessage in allPrivateMessages.filter({ !$0.read }) { @@ -39,25 +44,46 @@ struct UserMessageList: View { } try context.save() Logger.data.info("📖 [App] All unread direct messages marked as read for user \(user.num, privacy: .public).") - appState.unreadDirectMessages = user.unreadMessages + + if let connectedPeripheralNum = accessoryManager.activeDeviceNum, + let connectedNode = getNodeInfo(id: connectedPeripheralNum, context: context), + let connectedUser = connectedNode.user { + appState.unreadDirectMessages = connectedUser.unreadMessages(context: context, skipLastMessageCheck: true) // skipLastMessageCheck=true because we don't update lastMessage on our own connected node + } + context.refresh(user, mergeChanges: true) } catch { Logger.data.error("Failed to read direct messages: \(error.localizedDescription, privacy: .public)") } } - + + private func routerIsShowingThisUser() -> Bool { + guard router.navigationState.selectedTab == .messages else { return false } + return scenePhase == .active + } + var body: some View { + // Cast user.messageList to an array for easier indexing and ForEach. + let messages: [MessageEntity] = Array(allPrivateMessages) + + // Precompute previous message + let previousByID: [Int64: MessageEntity?] = { + var dict = [Int64: MessageEntity?]() + var prev: MessageEntity? + for m in messages { dict[m.messageId] = prev; prev = m } + return dict + }() + VStack { ScrollViewReader { scrollView in ScrollView { LazyVStack { - ForEach(allPrivateMessages.indices, id: \.self) { index in - let message = allPrivateMessages[index] - let previousMessage = index > 0 ? allPrivateMessages[index - 1] : nil + ForEach(messages, id: \.messageId) { message in + let previousMessage: MessageEntity? = previousByID[message.messageId] ?? nil UserMessageRow( message: message, - allMessages: allPrivateMessages, + allMessages: messages, previousMessage: previousMessage, preferredPeripheralNum: preferredPeripheralNum, user: user, @@ -80,7 +106,7 @@ struct UserMessageList: View { } } } - .id(redrawTapbacksTrigger) + } // Invisible spacer to detect reaching bottom Color.clear diff --git a/Meshtastic/Views/Nodes/Helpers/ShareContactQRDialog.swift b/Meshtastic/Views/Nodes/Helpers/ShareContactQRDialog.swift index 4bc56e24..cf30b441 100644 --- a/Meshtastic/Views/Nodes/Helpers/ShareContactQRDialog.swift +++ b/Meshtastic/Views/Nodes/Helpers/ShareContactQRDialog.swift @@ -13,12 +13,14 @@ import MeshtasticProtobufs import OSLog struct ShareContactQRDialog: View { + let manuallyVerified = false let node: NodeInfo @Environment(\.dismiss) private var dismiss var qrString: String { var contact = SharedContact() contact.nodeNum = node.num contact.user = node.user + contact.manuallyVerified = manuallyVerified do { let contactString = try contact.serializedData().base64EncodedString() return ("https://meshtastic.org/v/#" + contactString.base64ToBase64url()) diff --git a/Meshtastic/Views/Nodes/NodeList.swift b/Meshtastic/Views/Nodes/NodeList.swift index 34393253..f324ed8b 100644 --- a/Meshtastic/Views/Nodes/NodeList.swift +++ b/Meshtastic/Views/Nodes/NodeList.swift @@ -24,14 +24,14 @@ struct NodeList: View { @StateObject var filters = NodeFilterParameters() @State var isEditingFilters = false @SceneStorage("selectedDetailView") var selectedDetailView: String? - + var connectedNode: NodeInfoEntity? { if let num = accessoryManager.activeDeviceNum { return getNodeInfo(id: num, context: context) } return nil } - + var body: some View { NavigationSplitView { FilteredNodeList( @@ -137,7 +137,7 @@ struct NodeList: View { } } } - + // Helper to get the count of nodes for the navigation title private func getNodeCount() -> Int { let request: NSFetchRequest = NodeInfoEntity.fetchRequest() @@ -154,7 +154,7 @@ fileprivate struct FilteredNodeList: View { @EnvironmentObject var accessoryManager: AccessoryManager @FetchRequest private var nodes: FetchedResults @Environment(\.managedObjectContext) var context - + @Binding var selectedNode: NodeInfoEntity? var connectedNode: NodeInfoEntity? @Binding var isPresentingDeleteNodeAlert: Bool @@ -179,17 +179,19 @@ fileprivate struct FilteredNodeList: View { ] request.predicate = withFilters.buildPredicate() self._nodes = FetchRequest(fetchRequest: request) - + self._selectedNode = selectedNode self.connectedNode = connectedNode self._isPresentingDeleteNodeAlert = isPresentingDeleteNodeAlert self._deleteNodeId = deleteNodeId self._shareContactNode = shareContactNode } - + // The body of the view var body: some View { - List(nodes, id: \.self, selection: $selectedNode) { node in + // If the connected node passes filters, always show it first + let nodesWithConnectedFirst = nodes.filter { $0.num == accessoryManager.activeDeviceNum } + nodes.filter { $0.num != accessoryManager.activeDeviceNum } + List(nodesWithConnectedFirst, id: \.self, selection: $selectedNode) { node in NavigationLink(value: node) { NodeListItem( node: node, @@ -205,7 +207,7 @@ fileprivate struct FilteredNodeList: View { } } } - + @ViewBuilder func contextMenuActions( node: NodeInfoEntity, @@ -276,7 +278,7 @@ fileprivate struct FilteredNodeList: View { fileprivate extension NodeFilterParameters { func buildPredicate() -> NSPredicate? { var predicates: [NSPredicate] = [] - + // Search text predicates if !searchText.isEmpty { let searchKeys = [ @@ -288,19 +290,19 @@ fileprivate extension NodeFilterParameters { } predicates.append(NSCompoundPredicate(orPredicateWithSubpredicates: textPredicates)) } - + // Favorite filter if isFavorite { predicates.append(NSPredicate(format: "favorite == YES")) } - + // Via Lora/MQTT filters if viaLora && !viaMqtt { predicates.append(NSPredicate(format: "viaMqtt == NO")) } else if !viaLora && viaMqtt { predicates.append(NSPredicate(format: "viaMqtt == YES")) } - + // Role filter if roleFilter && !deviceRoles.isEmpty { let rolesPredicates = deviceRoles.map { @@ -308,41 +310,41 @@ fileprivate extension NodeFilterParameters { } predicates.append(NSCompoundPredicate(type: .or, subpredicates: rolesPredicates)) } - + // Hops Away filter if hopsAway == 0.0 { predicates.append(NSPredicate(format: "hopsAway == %i", 0)) } else if hopsAway > 0.0 { predicates.append(NSPredicate(format: "hopsAway > 0 AND hopsAway <= %i", Int32(hopsAway))) } - + // Online filter if isOnline { let isOnlinePredicate = NSPredicate(format: "lastHeard >= %@", Calendar.current.date(byAdding: .minute, value: -120, to: Date())! as NSDate) predicates.append(isOnlinePredicate) } - + // Encrypted filter if isPkiEncrypted { predicates.append(NSPredicate(format: "user.pkiEncrypted == YES")) } - + // Ignored filter if isIgnored { predicates.append(NSPredicate(format: "ignored == YES")) } else { predicates.append(NSPredicate(format: "ignored == NO")) } - + // Environment filter if isEnvironment { predicates.append(NSPredicate(format: "SUBQUERY(telemetries, $tel, $tel.metricsType == 1).@count > 0")) } - + // Distance filter if distanceFilter { if let pointOfInterest = LocationsHandler.currentLocation { - + if pointOfInterest.latitude != LocationsHandler.DefaultLocation.latitude && pointOfInterest.longitude != LocationsHandler.DefaultLocation.longitude { let d: Double = maxDistance * 1.1 let r: Double = 6371009 diff --git a/Meshtastic/Views/Onboarding/DeviceOnboarding.swift b/Meshtastic/Views/Onboarding/DeviceOnboarding.swift index 6f7c87a3..d95d6977 100644 --- a/Meshtastic/Views/Onboarding/DeviceOnboarding.swift +++ b/Meshtastic/Views/Onboarding/DeviceOnboarding.swift @@ -59,7 +59,7 @@ struct DeviceOnboarding: View { makeRow( icon: "person.2.shield", title: String(localized: "User Privacy"), - subtitle: String(localized: "Meshtastic does not collect any personal information. We do anonymously collect usage and crash data to improve the app. This helps us understand how the app is being used and where we can make improvements. The data we collect is non-personally identifiable and cannot be linked to you as an individual. You can opt out of this under app settings.") + subtitle: String(localized: "Meshtastic does not collect any personal information. We do anonymously collect usage and crash data to improve the app. You can opt out under app settings.") ) } .padding() diff --git a/Meshtastic/Views/Settings/AppSettings.swift b/Meshtastic/Views/Settings/AppSettings.swift index 4e1ab892..6a809b42 100644 --- a/Meshtastic/Views/Settings/AppSettings.swift +++ b/Meshtastic/Views/Settings/AppSettings.swift @@ -56,6 +56,9 @@ struct AppSettings: View { } .tint(.accentColor) } +#if targetEnvironment(macCatalyst) + +#else Button { isPresentingAppIconSheet.toggle() } label: { @@ -65,6 +68,7 @@ struct AppSettings: View { AppIconPicker(isPresenting: self.$isPresentingAppIconSheet) .presentationDetents([.medium]) } +#endif } Section(header: Text("environment")) { VStack(alignment: .leading) { diff --git a/Meshtastic/Views/Settings/SaveChannelQRCode.swift b/Meshtastic/Views/Settings/SaveChannelQRCode.swift index 71a032b4..cc4c6f84 100644 --- a/Meshtastic/Views/Settings/SaveChannelQRCode.swift +++ b/Meshtastic/Views/Settings/SaveChannelQRCode.swift @@ -9,6 +9,12 @@ import CoreData import OSLog import MeshtasticProtobufs +struct SaveChannelLinkData: Identifiable { + let id = UUID() + let data: String + let add: Bool +} + struct SaveChannelQRCode: View { @Environment(\.dismiss) private var dismiss @Environment(\.managedObjectContext) var context From 6705a184b4878bd7e7c0b6dc289a42382fb85828 Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Sat, 18 Oct 2025 11:08:31 -0700 Subject: [PATCH 03/68] Explicitly set unmessagable, seems unnessary --- Meshtastic/Extensions/CoreData/UserEntityExtension.swift | 1 + 1 file changed, 1 insertion(+) diff --git a/Meshtastic/Extensions/CoreData/UserEntityExtension.swift b/Meshtastic/Extensions/CoreData/UserEntityExtension.swift index 2a87ed9f..bba4075f 100644 --- a/Meshtastic/Extensions/CoreData/UserEntityExtension.swift +++ b/Meshtastic/Extensions/CoreData/UserEntityExtension.swift @@ -130,6 +130,7 @@ public func createUser(num: Int64, context: NSManagedObjectContext) throws -> Us newUser.longName = "Meshtastic \(last4)" newUser.shortName = last4 newUser.hwModel = "UNSET" + newUser.unmessagable = false } return newUser From 9e0a1ffea93188777d2a7e8c840b7c9343accdf7 Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Mon, 20 Oct 2025 11:33:58 -0700 Subject: [PATCH 04/68] Add back missing mesh map features --- Localizable.xcstrings | 8 --- .../Map/MapContent/MeshMapContent.swift | 49 ++++++++++++------- 2 files changed, 31 insertions(+), 26 deletions(-) diff --git a/Localizable.xcstrings b/Localizable.xcstrings index 4e78c69c..b7835609 100644 --- a/Localizable.xcstrings +++ b/Localizable.xcstrings @@ -3905,10 +3905,6 @@ } } }, - "Anonymous Usage and Crash data" : { - "comment" : "A description of how the app collects and uses data about its usage and crashes. It emphasizes that this data is anonymous and non-personally identifiable.", - "isCommentAutoGenerated" : true - }, "Any missed messages will be delivered again." : { "localizations" : { "it" : { @@ -41047,10 +41043,6 @@ }, "Waypoints" : { - }, - "We anonymously collect usage and crash data to improve the app. This helps us understand how the app is being used and where we can make improvements. The data we collect is non-personally identifiable and cannot be linked to you as an individual. You can opt out of this under app settings." : { - "comment" : "A description of how the app collects and uses user data. Includes a link to the app settings.", - "isCommentAutoGenerated" : true }, "Weather Conditions" : { "localizations" : { diff --git a/Meshtastic/Views/Nodes/Helpers/Map/MapContent/MeshMapContent.swift b/Meshtastic/Views/Nodes/Helpers/Map/MapContent/MeshMapContent.swift index 4941dd30..9ccf3f76 100644 --- a/Meshtastic/Views/Nodes/Helpers/Map/MapContent/MeshMapContent.swift +++ b/Meshtastic/Views/Nodes/Helpers/Map/MapContent/MeshMapContent.swift @@ -47,26 +47,39 @@ struct MeshMapContent: MapContent { @MapContentBuilder var positionAnnotations: some MapContent { ForEach(positions, id: \.id) { position in + /// Apply favorits filter and don't show ignored nodes if (!showFavorites || (position.nodePosition?.favorite == true)) && !(position.nodePosition?.ignored == true) { - - let nodeColor = UIColor(hex: UInt32(position.nodePosition?.num ?? 0)) - let positionName = position.nodePosition?.user?.longName ?? "?" - // Use a hash of the position ID to stagger animation delays for each node, preventing synchronized animations and improving visual distinction. - let calculatedDelay = Double(position.id.hashValue % 100) / 100.0 * 0.5 - - Annotation(positionName, coordinate: position.coordinate) { - LazyVStack { - AnimatedNodePin( - nodeColor: nodeColor, - shortName: position.nodePosition?.user?.shortName, - hasDetectionSensorMetrics: position.nodePosition?.hasDetectionSensorMetrics ?? false, - isOnline: position.nodePosition?.isOnline ?? false, - calculatedDelay: calculatedDelay - ) + if 12...15 ~= position.precisionBits || position.precisionBits == 32 { + + let nodeColor = UIColor(hex: UInt32(position.nodePosition?.num ?? 0)) + let positionName = position.nodePosition?.user?.longName ?? "?" + /// Reduced Precision Map Circle + if 12...15 ~= position.precisionBits { + let pp = PositionPrecision(rawValue: Int(position.precisionBits)) + let radius: CLLocationDistance = pp?.precisionMeters ?? 0 + if radius > 0.0 { + MapCircle(center: position.coordinate, radius: radius) + .foregroundStyle(Color(nodeColor).opacity(0.25)) + .stroke(.white, lineWidth: 1) + } + } + // Use a hash of the position ID to stagger animation delays for each node, preventing synchronized animations and improving visual distinction. + let calculatedDelay = Double(position.id.hashValue % 100) / 100.0 * 0.5 + + Annotation(positionName, coordinate: position.coordinate) { + LazyVStack { + AnimatedNodePin( + nodeColor: nodeColor, + shortName: position.nodePosition?.user?.shortName, + hasDetectionSensorMetrics: position.nodePosition?.hasDetectionSensorMetrics ?? false, + isOnline: position.nodePosition?.isOnline ?? false, + calculatedDelay: calculatedDelay + ) + } + .highPriorityGesture(TapGesture().onEnded { _ in + selectedPosition = (selectedPosition == position ? nil : position) + }) } - .highPriorityGesture(TapGesture().onEnded { _ in - selectedPosition = (selectedPosition == position ? nil : position) - }) } } } From 16e56e7f07044e2deb2465639392cfbe27dbba3d Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Mon, 20 Oct 2025 11:38:18 -0700 Subject: [PATCH 05/68] Fix: "Retrieving nodes" significantly slower after reconnect extracted from #1424 (#1477) * Fix: "Retrieving nodes" significantly slower after reconnect (#1424) The node database retrieval was calling context.save() for every single NodeInfo packet received (250 saves for 250 nodes). This caused severe performance degradation on reconnect when CoreData had accumulated state. Root Cause: - nodeInfoPacket() called context.save() immediately for each node - With 250 nodes, this meant 250 individual CoreData save operations - On first connection, CoreData is fresh and fast - On reconnect, CoreData has accumulated change tracking, undo management, and memory pressure, making each save progressively slower - This resulted in 10+ second retrieval times vs 1-2 seconds initially Solution: - Added deferSave parameter to nodeInfoPacket() function - During database retrieval (.retrievingDatabase state), defer all saves - Perform a single batch save when database retrieval completes (when NONCE_ONLY_DB configCompleteID is received) - This reduces 250 saves to 1 save Performance Impact: - Eliminates N individual saves during node database sync - Reduces database retrieval time back to 1-2 seconds on reconnect - Matches first-connection performance consistently Fixes #1424 * Revert *MessageListUnified files --------- Co-authored-by: Martin Bogomolni Co-authored-by: Jake-B --- .../AccessoryManager+FromRadio.swift | 5 ++++- .../Accessory Manager/AccessoryManager.swift | 11 +++++++++++ Meshtastic/Helpers/MeshPackets.swift | 14 +++++++++----- 3 files changed, 24 insertions(+), 6 deletions(-) diff --git a/Meshtastic/Accessory/Accessory Manager/AccessoryManager+FromRadio.swift b/Meshtastic/Accessory/Accessory Manager/AccessoryManager+FromRadio.swift index 01cdac79..539f4a5e 100644 --- a/Meshtastic/Accessory/Accessory Manager/AccessoryManager+FromRadio.swift +++ b/Meshtastic/Accessory/Accessory Manager/AccessoryManager+FromRadio.swift @@ -106,8 +106,11 @@ extension AccessoryManager { return } + // Check if we're in database retrieval mode to defer saves for performance + let isRetrievingDatabase = if case .retrievingDatabase = self.state { true } else { false } + // TODO: nodeInfoPacket's channel: parameter is not used - if let nodeInfo = nodeInfoPacket(nodeInfo: nodeInfo, channel: 0, context: context) { + if let nodeInfo = nodeInfoPacket(nodeInfo: nodeInfo, channel: 0, context: context, deferSave: isRetrievingDatabase) { if let activeDevice = activeConnection?.device, activeDevice.num == nodeInfo.num { if let user = nodeInfo.user { updateDevice(deviceId: activeDevice.id, key: \.shortName, value: user.shortName ?? "?") diff --git a/Meshtastic/Accessory/Accessory Manager/AccessoryManager.swift b/Meshtastic/Accessory/Accessory Manager/AccessoryManager.swift index 3c507a03..88b70032 100644 --- a/Meshtastic/Accessory/Accessory Manager/AccessoryManager.swift +++ b/Meshtastic/Accessory/Accessory Manager/AccessoryManager.swift @@ -665,6 +665,17 @@ class AccessoryManager: ObservableObject, MqttClientProxyManagerDelegate { self.firstDatabaseNodeInfoContinuation = nil } + // Perform a single batch save after database retrieval completes + // This significantly improves performance on reconnect + do { + try context.save() + Logger.data.info("💾 [Database] Batch saved all node info after database retrieval") + } catch { + context.rollback() + let nsError = error as NSError + Logger.data.error("💥 [Database] Error saving batch node info: \(nsError, privacy: .public)") + } + default: Logger.transport.error("[Accessory] Unknown nonce completed: \(configCompleteID)") } diff --git a/Meshtastic/Helpers/MeshPackets.swift b/Meshtastic/Helpers/MeshPackets.swift index caffd8b4..e3aa3252 100644 --- a/Meshtastic/Helpers/MeshPackets.swift +++ b/Meshtastic/Helpers/MeshPackets.swift @@ -264,7 +264,7 @@ func deviceMetadataPacket (metadata: DeviceMetadata, fromNum: Int64, sessionPass } } -func nodeInfoPacket (nodeInfo: NodeInfo, channel: UInt32, context: NSManagedObjectContext) -> NodeInfoEntity? { +func nodeInfoPacket (nodeInfo: NodeInfo, channel: UInt32, context: NSManagedObjectContext, deferSave: Bool = false) -> NodeInfoEntity? { let logString = String.localizedStringWithFormat("[NodeInfo] received for: %@".localized, String(nodeInfo.num)) Logger.mesh.info("📟 \(logString, privacy: .public)") @@ -375,8 +375,10 @@ func nodeInfoPacket (nodeInfo: NodeInfo, channel: UInt32, context: NSManagedObje newNode.myInfo = fetchedMyInfo[0] } do { - try context.save() - Logger.data.info("💾 Saved a new Node Info For: \(String(nodeInfo.num), privacy: .public)") + if !deferSave { + try context.save() + Logger.data.info("💾 Saved a new Node Info For: \(String(nodeInfo.num), privacy: .public)") + } return newNode } catch { context.rollback() @@ -500,8 +502,10 @@ func nodeInfoPacket (nodeInfo: NodeInfo, channel: UInt32, context: NSManagedObje fetchedNode[0].myInfo = fetchedMyInfo[0] } do { - try context.save() - Logger.data.info("💾 [NodeInfo] saved for \(nodeInfo.num.toHex(), privacy: .public)") + if !deferSave { + try context.save() + Logger.data.info("💾 [NodeInfo] saved for \(nodeInfo.num.toHex(), privacy: .public)") + } return fetchedNode[0] } catch { context.rollback() From 4114722ebf1add0199aa3f8407c7728685297fa2 Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Tue, 21 Oct 2025 06:03:01 -0700 Subject: [PATCH 06/68] Hide route lines filter from mesh map --- .../Views/Nodes/Helpers/Map/MapSettingsForm.swift | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/Meshtastic/Views/Nodes/Helpers/Map/MapSettingsForm.swift b/Meshtastic/Views/Nodes/Helpers/Map/MapSettingsForm.swift index 304c5e69..ca17c6fa 100644 --- a/Meshtastic/Views/Nodes/Helpers/Map/MapSettingsForm.swift +++ b/Meshtastic/Views/Nodes/Helpers/Map/MapSettingsForm.swift @@ -84,15 +84,12 @@ struct MapSettingsForm: View { Label("Node History", systemImage: "building.columns.fill") } .toggleStyle(SwitchToggleStyle(tint: .accentColor)) - .onTapGesture { - self.nodeHistory.toggle() - UserDefaults.enableMapNodeHistoryPins = self.nodeHistory + Toggle(isOn: $enableMapRouteLines) { + Label("Route Lines", systemImage: "road.lanes") } + .tint(.accentColor) + } - Toggle(isOn: $enableMapRouteLines) { - Label("Route Lines", systemImage: "road.lanes") - } - .tint(.accentColor) Toggle(isOn: $convexHull) { Label("Convex Hull", systemImage: "button.angledbottom.horizontal.right") } From 6c3c02237b8b33ef9dded5ee0dc393115eeaf4c0 Mon Sep 17 00:00:00 2001 From: Mike Robbins Date: Tue, 21 Oct 2025 09:23:22 -0400 Subject: [PATCH 07/68] Mesh Map: fuzz imprecise locations so they're distinguishable and clickable at the highest zoom levels (#1478) Co-authored-by: Garth Vander Houwen --- .../CoreData/PositionEntityExtension.swift | 30 +++++++++++++++++++ .../Map/MapContent/MeshMapContent.swift | 23 ++++++++++++++ 2 files changed, 53 insertions(+) diff --git a/Meshtastic/Extensions/CoreData/PositionEntityExtension.swift b/Meshtastic/Extensions/CoreData/PositionEntityExtension.swift index 3efa9af3..080693c2 100644 --- a/Meshtastic/Extensions/CoreData/PositionEntityExtension.swift +++ b/Meshtastic/Extensions/CoreData/PositionEntityExtension.swift @@ -96,10 +96,40 @@ extension PositionEntity { } return pointAnn } + + var isPreciseLocation: Bool { + precisionBits == 32 || precisionBits == 0 + } + + var fuzzedNodeCoordinate: CLLocationCoordinate2D? { + // With reduced precisionBits, many nodes can overlap on the map, making them unclickable. + // Use a hash of the position ID to fuzz coordinate slightly so that these nodes can be distinguished at the higest zoom levels. This allows them to be clicked individually. + if latitudeI != 0 && longitudeI != 0 { + // Derive two uniform pseudorandom numbers [0,1) from id.hashValue + let u1 = Double(id.hashValue & 0xFFFF) / 65536.0 + let u2 = Double((id.hashValue >> 16) & 0xFFFF) / 65536.0 + + // Angle and radius + let offsetAngle = 2.0 * .pi * u1 + let offsetRadius = 0.00001 * sqrt(u2) // 1.0e-5 degrees at equator is about 1.11 m or 4 ft + + let dLat = sin(offsetAngle) * offsetRadius + let dLon = cos(offsetAngle) * offsetRadius + + let coord = CLLocationCoordinate2D( + latitude: latitude! + dLat, + longitude: longitude! + dLon + ) + return coord + } else { + return nil + } + } } extension PositionEntity: MKAnnotation { public var coordinate: CLLocationCoordinate2D { nodeCoordinate ?? LocationsHandler.DefaultLocation } + public var fuzzedCoordinate: CLLocationCoordinate2D { fuzzedNodeCoordinate ?? LocationsHandler.DefaultLocation } public var title: String? { nodePosition?.user?.shortName ?? "Unknown".localized } public var subtitle: String? { time?.formatted() } } diff --git a/Meshtastic/Views/Nodes/Helpers/Map/MapContent/MeshMapContent.swift b/Meshtastic/Views/Nodes/Helpers/Map/MapContent/MeshMapContent.swift index 9ccf3f76..4edb6ac4 100644 --- a/Meshtastic/Views/Nodes/Helpers/Map/MapContent/MeshMapContent.swift +++ b/Meshtastic/Views/Nodes/Helpers/Map/MapContent/MeshMapContent.swift @@ -49,6 +49,29 @@ struct MeshMapContent: MapContent { ForEach(positions, id: \.id) { position in /// Apply favorits filter and don't show ignored nodes if (!showFavorites || (position.nodePosition?.favorite == true)) && !(position.nodePosition?.ignored == true) { + + let nodeColor = UIColor(hex: UInt32(position.nodePosition?.num ?? 0)) + let positionName = position.nodePosition?.user?.longName ?? "?" + // Use a hash of the position ID to stagger animation delays for each node, preventing synchronized animations and improving visual distinction. + let calculatedDelay = Double(position.id.hashValue % 100) / 100.0 * 0.5 + + let coordinateForNodePin: CLLocationCoordinate2D = if position.isPreciseLocation { + // Precise location: place node pin at actual location. + position.coordinate + } else { + // Imprecise location: fuzz slightly so overlapping nodes are visible and clickable at highest zoom levels. + position.fuzzedCoordinate + } + + Annotation(positionName, coordinate: coordinateForNodePin) { + LazyVStack { + AnimatedNodePin( + nodeColor: nodeColor, + shortName: position.nodePosition?.user?.shortName, + hasDetectionSensorMetrics: position.nodePosition?.hasDetectionSensorMetrics ?? false, + isOnline: position.nodePosition?.isOnline ?? false, + calculatedDelay: calculatedDelay + ) if 12...15 ~= position.precisionBits || position.precisionBits == 32 { let nodeColor = UIColor(hex: UInt32(position.nodePosition?.num ?? 0)) From 4aa56b15311a43389ff79d8565f97667e2f044aa Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Tue, 21 Oct 2025 06:28:33 -0700 Subject: [PATCH 08/68] Fix bad merge --- .../Map/MapContent/MeshMapContent.swift | 18 +----------------- 1 file changed, 1 insertion(+), 17 deletions(-) diff --git a/Meshtastic/Views/Nodes/Helpers/Map/MapContent/MeshMapContent.swift b/Meshtastic/Views/Nodes/Helpers/Map/MapContent/MeshMapContent.swift index 4edb6ac4..707ee1da 100644 --- a/Meshtastic/Views/Nodes/Helpers/Map/MapContent/MeshMapContent.swift +++ b/Meshtastic/Views/Nodes/Helpers/Map/MapContent/MeshMapContent.swift @@ -49,12 +49,6 @@ struct MeshMapContent: MapContent { ForEach(positions, id: \.id) { position in /// Apply favorits filter and don't show ignored nodes if (!showFavorites || (position.nodePosition?.favorite == true)) && !(position.nodePosition?.ignored == true) { - - let nodeColor = UIColor(hex: UInt32(position.nodePosition?.num ?? 0)) - let positionName = position.nodePosition?.user?.longName ?? "?" - // Use a hash of the position ID to stagger animation delays for each node, preventing synchronized animations and improving visual distinction. - let calculatedDelay = Double(position.id.hashValue % 100) / 100.0 * 0.5 - let coordinateForNodePin: CLLocationCoordinate2D = if position.isPreciseLocation { // Precise location: place node pin at actual location. position.coordinate @@ -62,16 +56,6 @@ struct MeshMapContent: MapContent { // Imprecise location: fuzz slightly so overlapping nodes are visible and clickable at highest zoom levels. position.fuzzedCoordinate } - - Annotation(positionName, coordinate: coordinateForNodePin) { - LazyVStack { - AnimatedNodePin( - nodeColor: nodeColor, - shortName: position.nodePosition?.user?.shortName, - hasDetectionSensorMetrics: position.nodePosition?.hasDetectionSensorMetrics ?? false, - isOnline: position.nodePosition?.isOnline ?? false, - calculatedDelay: calculatedDelay - ) if 12...15 ~= position.precisionBits || position.precisionBits == 32 { let nodeColor = UIColor(hex: UInt32(position.nodePosition?.num ?? 0)) @@ -89,7 +73,7 @@ struct MeshMapContent: MapContent { // Use a hash of the position ID to stagger animation delays for each node, preventing synchronized animations and improving visual distinction. let calculatedDelay = Double(position.id.hashValue % 100) / 100.0 * 0.5 - Annotation(positionName, coordinate: position.coordinate) { + Annotation(positionName, coordinate: coordinateForNodePin) { LazyVStack { AnimatedNodePin( nodeColor: nodeColor, From ddb01f5d5ff9e3a7a5df4b8bc6a97cd82366cd24 Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Sun, 26 Oct 2025 18:40:19 -0700 Subject: [PATCH 09/68] Update Meshtastic/Extensions/CoreData/UserEntityExtension.swift Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- Meshtastic/Extensions/CoreData/UserEntityExtension.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Meshtastic/Extensions/CoreData/UserEntityExtension.swift b/Meshtastic/Extensions/CoreData/UserEntityExtension.swift index 48e44e30..1307764a 100644 --- a/Meshtastic/Extensions/CoreData/UserEntityExtension.swift +++ b/Meshtastic/Extensions/CoreData/UserEntityExtension.swift @@ -56,7 +56,7 @@ extension UserEntity { guard self.lastMessage != nil || skipLastMessageCheck else { return 0; } let fetchRequest = messageFetchRequest - fetchRequest.sortDescriptors = [] // sort is irrelvant. + fetchRequest.sortDescriptors = [] // sort is irrelevant. fetchRequest.predicate = NSCompoundPredicate(andPredicateWithSubpredicates: [fetchRequest.predicate!, NSPredicate(format: "read == false")]) return (try? context.count(for: fetchRequest)) ?? 0 From 428b144701d69b33973744dbea3e39abb2917256 Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Sun, 26 Oct 2025 18:40:37 -0700 Subject: [PATCH 10/68] Update Meshtastic/Views/Nodes/Helpers/Map/MapContent/MeshMapContent.swift Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- .../Views/Nodes/Helpers/Map/MapContent/MeshMapContent.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Meshtastic/Views/Nodes/Helpers/Map/MapContent/MeshMapContent.swift b/Meshtastic/Views/Nodes/Helpers/Map/MapContent/MeshMapContent.swift index 707ee1da..f1fd931f 100644 --- a/Meshtastic/Views/Nodes/Helpers/Map/MapContent/MeshMapContent.swift +++ b/Meshtastic/Views/Nodes/Helpers/Map/MapContent/MeshMapContent.swift @@ -47,7 +47,7 @@ struct MeshMapContent: MapContent { @MapContentBuilder var positionAnnotations: some MapContent { ForEach(positions, id: \.id) { position in - /// Apply favorits filter and don't show ignored nodes + /// Apply favorites filter and don't show ignored nodes if (!showFavorites || (position.nodePosition?.favorite == true)) && !(position.nodePosition?.ignored == true) { let coordinateForNodePin: CLLocationCoordinate2D = if position.isPreciseLocation { // Precise location: place node pin at actual location. From 4facf1054b88dc2ccaa45a52f27fdfd0a66e5288 Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Sun, 26 Oct 2025 18:41:13 -0700 Subject: [PATCH 11/68] Update Meshtastic/Extensions/CoreData/ChannelEntityExtension.swift Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- Meshtastic/Extensions/CoreData/ChannelEntityExtension.swift | 1 - 1 file changed, 1 deletion(-) diff --git a/Meshtastic/Extensions/CoreData/ChannelEntityExtension.swift b/Meshtastic/Extensions/CoreData/ChannelEntityExtension.swift index 8d7962b4..3d0fc0f1 100644 --- a/Meshtastic/Extensions/CoreData/ChannelEntityExtension.swift +++ b/Meshtastic/Extensions/CoreData/ChannelEntityExtension.swift @@ -38,7 +38,6 @@ extension ChannelEntity { } func unreadMessages(context: NSManagedObjectContext) -> Int { - let context = PersistenceController.shared.container.viewContext let fetchRequest = messageFetchRequest fetchRequest.sortDescriptors = [] // sort is irrelvant. fetchRequest.predicate = NSCompoundPredicate(andPredicateWithSubpredicates: [fetchRequest.predicate!, NSPredicate(format: "read == false")]) From 5ecad218c6f8d2685ceb492b468a5b25fcfba70b Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Sun, 26 Oct 2025 18:41:26 -0700 Subject: [PATCH 12/68] Update Meshtastic/Extensions/CoreData/MyInfoEntityExtension.swift Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- Meshtastic/Extensions/CoreData/MyInfoEntityExtension.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Meshtastic/Extensions/CoreData/MyInfoEntityExtension.swift b/Meshtastic/Extensions/CoreData/MyInfoEntityExtension.swift index 136b64cb..c9e06d88 100644 --- a/Meshtastic/Extensions/CoreData/MyInfoEntityExtension.swift +++ b/Meshtastic/Extensions/CoreData/MyInfoEntityExtension.swift @@ -30,7 +30,7 @@ extension MyInfoEntity { func unreadMessages(context: NSManagedObjectContext) -> Int { // Returns the count of unread *channel* messages let fetchRequest = messageFetchRequest - fetchRequest.sortDescriptors = [] // sort is irrelvant. + fetchRequest.sortDescriptors = [] // sort is irrelevant. fetchRequest.predicate = NSCompoundPredicate(andPredicateWithSubpredicates: [fetchRequest.predicate!, NSPredicate(format: "read == false")]) return (try? context.count(for: fetchRequest)) ?? 0 From 0c3f1bd2d646c48373aa96fd4a3b8faa3a349c32 Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Sun, 26 Oct 2025 18:41:34 -0700 Subject: [PATCH 13/68] Update Meshtastic/Extensions/CoreData/ChannelEntityExtension.swift Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- Meshtastic/Extensions/CoreData/ChannelEntityExtension.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Meshtastic/Extensions/CoreData/ChannelEntityExtension.swift b/Meshtastic/Extensions/CoreData/ChannelEntityExtension.swift index 3d0fc0f1..5916567c 100644 --- a/Meshtastic/Extensions/CoreData/ChannelEntityExtension.swift +++ b/Meshtastic/Extensions/CoreData/ChannelEntityExtension.swift @@ -39,7 +39,7 @@ extension ChannelEntity { func unreadMessages(context: NSManagedObjectContext) -> Int { let fetchRequest = messageFetchRequest - fetchRequest.sortDescriptors = [] // sort is irrelvant. + fetchRequest.sortDescriptors = [] // sort is irrelevant. fetchRequest.predicate = NSCompoundPredicate(andPredicateWithSubpredicates: [fetchRequest.predicate!, NSPredicate(format: "read == false")]) return (try? context.count(for: fetchRequest)) ?? 0 From 3f27e3b9259cb51278856e8dbcd2e288f89c7057 Mon Sep 17 00:00:00 2001 From: jake-b <1012393+jake-b@users.noreply.github.com> Date: Tue, 28 Oct 2025 09:18:17 -0400 Subject: [PATCH 14/68] Keep list of previous manual connections (#1484) * Keep list of previous manual connections * More descriptive manual connection rows * Merge fixes and new way to show IP on Connect view --------- Co-authored-by: Jake-B --- Localizable.xcstrings | 14 +- Meshtastic.xcodeproj/project.pbxproj | 4 + .../AccessoryManager+Connect.swift | 16 +- .../AccessoryManager+FromRadio.swift | 9 + .../Accessory Manager/AccessoryManager.swift | 11 +- .../Helpers/ManualConnectionList.swift | 61 +++++ .../Accessory/Protocols/Connection.swift | 2 +- Meshtastic/Accessory/Protocols/Device.swift | 22 +- .../Accessory/Protocols/Transport.swift | 9 +- .../Bluetooth Low Energy/BLETransport.swift | 6 +- .../Transports/Serial/SerialTransport.swift | 13 +- .../Transports/TCP/TCPTransport.swift | 57 ++++- Meshtastic/Extensions/Bundle.swift | 8 + Meshtastic/Extensions/UserDefaults.swift | 31 +++ Meshtastic/Views/Connect/Connect.swift | 223 +++++++++++------- 15 files changed, 372 insertions(+), 114 deletions(-) create mode 100644 Meshtastic/Accessory/Helpers/ManualConnectionList.swift diff --git a/Localizable.xcstrings b/Localizable.xcstrings index b7835609..6f4a50b6 100644 --- a/Localizable.xcstrings +++ b/Localizable.xcstrings @@ -18921,6 +18921,13 @@ } } } + }, + "Last seen device:" : { + "comment" : "A label displayed next to the last seen device text in the `DeviceConnectRow`.", + "isCommentAutoGenerated" : true + }, + "Last seen device: %@" : { + }, "Latitude" : { "localizations" : { @@ -20441,6 +20448,9 @@ } } } + }, + "Manual Connections" : { + }, "Map Data" : { "localizations" : { @@ -20926,8 +20936,8 @@ } } }, - "Meshtastic does not collect any personal information. We do anonymously collect usage and crash data to improve the app. You can opt out under app settings." : { - "comment" : "A description of Meshtastic's data collection practices.", + "Meshtastic does not collect any personal information. We do anonymously collect usage and crash data to improve the app. This helps us understand how the app is being used and where we can make improvements. The data we collect is non-personally identifiable and cannot be linked to you as an individual. You can opt out of this under app settings." : { + "comment" : "Privacy policy text for Meshtastic.", "isCommentAutoGenerated" : true }, "Meshtastic Node %@ has shared channels with you" : { diff --git a/Meshtastic.xcodeproj/project.pbxproj b/Meshtastic.xcodeproj/project.pbxproj index 7a4a216f..ee8aba2a 100644 --- a/Meshtastic.xcodeproj/project.pbxproj +++ b/Meshtastic.xcodeproj/project.pbxproj @@ -42,6 +42,7 @@ 2344A2B12D68DFF800170A77 /* Constants.swift in Sources */ = {isa = PBXBuildFile; fileRef = 25C49D8F2C471AEA0024FBD1 /* Constants.swift */; }; 2346A7192E2FB9A300CB9239 /* SerialConnection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2346A7182E2FB9A300CB9239 /* SerialConnection.swift */; }; 2346A71D2E2FB9C500CB9239 /* SerialTransport.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2346A71C2E2FB9C500CB9239 /* SerialTransport.swift */; }; + 2349A04A2EAE4DA30060A581 /* ManualConnectionList.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2349A0492EAE4DA30060A581 /* ManualConnectionList.swift */; }; 2373AE132D0A216C0086C749 /* MetricsChartSeries.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2373AE122D0A216C0086C749 /* MetricsChartSeries.swift */; }; 2373AE152D0A24930086C749 /* MetricsSeriesList.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2373AE142D0A24930086C749 /* MetricsSeriesList.swift */; }; 2373AE172D0A26620086C749 /* EnvironmentDefaultSeries.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2373AE162D0A26620086C749 /* EnvironmentDefaultSeries.swift */; }; @@ -357,6 +358,7 @@ 2344A2AE2D6697A700170A77 /* TelemetryEntity+CoreDataProperties.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "TelemetryEntity+CoreDataProperties.swift"; sourceTree = ""; }; 2346A7182E2FB9A300CB9239 /* SerialConnection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SerialConnection.swift; sourceTree = ""; }; 2346A71C2E2FB9C500CB9239 /* SerialTransport.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SerialTransport.swift; sourceTree = ""; }; + 2349A0492EAE4DA30060A581 /* ManualConnectionList.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ManualConnectionList.swift; sourceTree = ""; }; 2373AE122D0A216C0086C749 /* MetricsChartSeries.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MetricsChartSeries.swift; sourceTree = ""; }; 2373AE142D0A24930086C749 /* MetricsSeriesList.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MetricsSeriesList.swift; sourceTree = ""; }; 2373AE162D0A26620086C749 /* EnvironmentDefaultSeries.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EnvironmentDefaultSeries.swift; sourceTree = ""; }; @@ -832,6 +834,7 @@ 23D316922E5618D2002FA4FB /* AsyncGate.swift */, 23E23F912E392C2B00919073 /* LogRecord+StringRepresentation.swift */, 23D9D9382E50DA97005D1C18 /* ResettableTimer.swift */, + 2349A0492EAE4DA30060A581 /* ManualConnectionList.swift */, ); path = Helpers; sourceTree = ""; @@ -1841,6 +1844,7 @@ BCDDFA9A2DBB180D0065189C /* ScrollToBottomButton.swift in Sources */, DDC4C9FF2A8D982900CE201C /* DetectionSensorConfig.swift in Sources */, 2344A2AF2D6697A700170A77 /* TelemetryEntity+CoreDataClass.swift in Sources */, + 2349A04A2EAE4DA30060A581 /* ManualConnectionList.swift in Sources */, 2344A2B02D6697A700170A77 /* TelemetryEntity+CoreDataProperties.swift in Sources */, D9C983A22B79D1A600BDBE6A /* RequestPositionButton.swift in Sources */, 237AEB8F2E1FE457003B7CE3 /* Transport.swift in Sources */, diff --git a/Meshtastic/Accessory/Accessory Manager/AccessoryManager+Connect.swift b/Meshtastic/Accessory/Accessory Manager/AccessoryManager+Connect.swift index df627fbe..d63f8acf 100644 --- a/Meshtastic/Accessory/Accessory Manager/AccessoryManager+Connect.swift +++ b/Meshtastic/Accessory/Accessory Manager/AccessoryManager+Connect.swift @@ -67,7 +67,12 @@ extension AccessoryManager { } self.activeConnection = (device: device, connection: connection) - if UserDefaults.preferredPeripheralId.count < 1 { + // if we don't have a peripheralId, set it now at the beginning of the + // connect process (because I think it is used in other parts of the app + // during the connect process? + // Otherwise, UserDefault.preferredPeripheralId is set in the Connect + // view, as part of the "Connect to new radio?" confirmation dialog logic. + if UserDefaults.preferredPeripheralId.isEmpty { UserDefaults.preferredPeripheralId = device.id.uuidString } } catch let error as CBError where error.code == .peerRemovedPairingInformation { @@ -168,6 +173,15 @@ extension AccessoryManager { // We have an active connection self.updateDevice(deviceId: device.id, key: \.connectionState, value: .connected) self.updateState(.subscribed) + + // If we successfully connected to a manual connection, then save it to the list + // Remember, Device is a value type (struct) so don't use use `device` here, thats + // The value at the instantiation of the connect process. We want the currently + // updated device object in `activeConnection` with its additonal metadata from + // NodeInfo packets. + if let activeDevice = self.activeConnection?.device, activeDevice.isManualConnection { + ManualConnectionList.shared.insert(device: activeDevice) + } } // Step 8: Update UI and status to connected diff --git a/Meshtastic/Accessory/Accessory Manager/AccessoryManager+FromRadio.swift b/Meshtastic/Accessory/Accessory Manager/AccessoryManager+FromRadio.swift index 539f4a5e..5bcead9b 100644 --- a/Meshtastic/Accessory/Accessory Manager/AccessoryManager+FromRadio.swift +++ b/Meshtastic/Accessory/Accessory Manager/AccessoryManager+FromRadio.swift @@ -116,6 +116,15 @@ extension AccessoryManager { updateDevice(deviceId: activeDevice.id, key: \.shortName, value: user.shortName ?? "?") updateDevice(deviceId: activeDevice.id, key: \.longName, value: user.longName ?? "Unknown".localized) updateDevice(deviceId: activeDevice.id, key: \.hardwareModel, value: user.hwModel) + + if activeDevice.isManualConnection { + // We just received a NodeInfo for the currently connected node and this is a + // manual connection. Update the metadata for the device entry in UserDefaults + // with this information for better display later + ManualConnectionList.shared.updateDevice(deviceId: activeDevice.id, key: \.shortName, value: user.shortName) + ManualConnectionList.shared.updateDevice(deviceId: activeDevice.id, key: \.longName, value: user.longName) + ManualConnectionList.shared.updateDevice(deviceId: activeDevice.id, key: \.hardwareModel, value: user.hwModel) + } } } } diff --git a/Meshtastic/Accessory/Accessory Manager/AccessoryManager.swift b/Meshtastic/Accessory/Accessory Manager/AccessoryManager.swift index 88b70032..f5a114ba 100644 --- a/Meshtastic/Accessory/Accessory Manager/AccessoryManager.swift +++ b/Meshtastic/Accessory/Accessory Manager/AccessoryManager.swift @@ -302,9 +302,9 @@ class AccessoryManager: ObservableObject, MqttClientProxyManagerDelegate { Logger.transport.error("updateDevice with nil deviceId") return } - - // Update the active device - if let activeConnection { + + // Update the active device if the UUID's match + if let activeConnection, activeConnection.device.id == deviceId { var device = activeConnection.device if device[keyPath: key] != value { // Update the @Published stuff for the UI @@ -714,7 +714,8 @@ extension AccessoryManager { await self.heartbeatTimer?.cancel(withReason: "Duplicate setup, cancelling previous timer") self.heartbeatTimer = nil } - self.heartbeatTimer = ResettableTimer(isRepeating: true, debugName: "Send Heartbeat") { + + self.heartbeatTimer = ResettableTimer(isRepeating: true, debugName: Bundle.main.isDebug ? "Send Heartbeat" : nil) { Logger.transport.debug("💓 [Heartbeat] Sending periodic heartbeat") try? await self.sendHeartbeat() } @@ -722,7 +723,7 @@ extension AccessoryManager { // We can send heartbeats for older versions just fine, but only 2.7.4 and up will respond with // a definite queueStatus packet. if self.checkIsVersionSupported(forVersion: "2.7.4") { - self.heartbeatResponseTimer = ResettableTimer(isRepeating: false, debugName: "Heartbeat Timeout") { @MainActor in + self.heartbeatResponseTimer = ResettableTimer(isRepeating: false, debugName: Bundle.main.isDebug ? "Heartbeat Timeout" : nil) { @MainActor in Logger.transport.error("💓 [Heartbeat] Connection Timeout: Did not receive a packet after heartbeat.") // If we're in the middle of a connection cancel it. await self.connectionStepper?.cancel() diff --git a/Meshtastic/Accessory/Helpers/ManualConnectionList.swift b/Meshtastic/Accessory/Helpers/ManualConnectionList.swift new file mode 100644 index 00000000..665573a9 --- /dev/null +++ b/Meshtastic/Accessory/Helpers/ManualConnectionList.swift @@ -0,0 +1,61 @@ +// +// ManualConnectionList.swift +// Meshtastic +// +// Created by jake on 10/26/25. +// + +import Foundation + +// Maintains an observable list of devices that's backed by UserDefaults +public class ManualConnectionList: ObservableObject { + static let shared = ManualConnectionList() + + @Published private var _list: [Device] + + private init() { + _list = UserDefaults.manualConnections + } + + var connectionsList: [Device] { + get { + return _list + } + } + + func insert(device: Device) { + // Don't insert if already there + guard !_list.contains(where: {$0.id == device.id}) else { + return + } + + // Add the new entry + var list = _list + list.append(device) + _list = list + UserDefaults.manualConnections = list + } + + func updateDevice(deviceId: UUID, key: WritableKeyPath, value: T) where T: Equatable { + var list = _list + if let deviceIndex = list.firstIndex(where: {$0.id == deviceId}) { + list[deviceIndex][keyPath: key] = value + _list = list + UserDefaults.manualConnections = list + } + } + + func remove(device: Device) { + var list = _list + list.removeAll(where: {$0.id == device.id}) + _list = list + UserDefaults.manualConnections = list + } + + func remove(atOffsets: IndexSet) { + var list = _list + list.remove(atOffsets: atOffsets) + _list = list + UserDefaults.manualConnections = list + } +} diff --git a/Meshtastic/Accessory/Protocols/Connection.swift b/Meshtastic/Accessory/Protocols/Connection.swift index afc087c5..27758b84 100644 --- a/Meshtastic/Accessory/Protocols/Connection.swift +++ b/Meshtastic/Accessory/Protocols/Connection.swift @@ -31,7 +31,7 @@ enum ConnectionEvent { case disconnected(shouldReconnect: Bool) } -enum ConnectionState: Equatable { +enum ConnectionState: Equatable, Codable { case disconnected case connecting case connected diff --git a/Meshtastic/Accessory/Protocols/Device.swift b/Meshtastic/Accessory/Protocols/Device.swift index 844eff31..d14b349e 100644 --- a/Meshtastic/Accessory/Protocols/Device.swift +++ b/Meshtastic/Accessory/Protocols/Device.swift @@ -7,7 +7,8 @@ import Foundation -struct Device: Identifiable, Hashable { +struct Device: Identifiable, Hashable, Codable, CustomStringConvertible { + let id: UUID var name: String var transportType: TransportType @@ -23,10 +24,9 @@ struct Device: Identifiable, Hashable { var connectionState: ConnectionState var wasRestored: Bool = false + var isManualConnection: Bool = false - var connectionDetails: String? - - init(id: UUID, name: String, transportType: TransportType, identifier: String, connectionState: ConnectionState = .disconnected, rssi: Int? = nil, num: Int64? = nil, connectionDetails: String? = nil, wasRestored: Bool = false) { + init(id: UUID, name: String, transportType: TransportType, identifier: String, connectionState: ConnectionState = .disconnected, rssi: Int? = nil, num: Int64? = nil, wasRestored: Bool = false, isManualConnection: Bool = false) { self.id = id self.name = name self.transportType = transportType @@ -35,7 +35,7 @@ struct Device: Identifiable, Hashable { self.rssi = rssi self.num = num self.wasRestored = wasRestored - self.connectionDetails = connectionDetails + self.isManualConnection = isManualConnection } var rssiString: String { @@ -57,4 +57,16 @@ struct Device: Identifiable, Hashable { } } + var description: String { + switch (shortName, longName) { + case (let shortName?, let longName?): // Both shortName and longName are non-nil + return "\(longName) (\(shortName))" + case (let shortName?, nil): // shortName is non-nil, longName is nil + return "\(shortName)" + case (nil, let longName?): // shortName is nil, longName is non-nil + return "\(longName)" + default: // Both are nil + return "Device(id: \(id))" + } + } } diff --git a/Meshtastic/Accessory/Protocols/Transport.swift b/Meshtastic/Accessory/Protocols/Transport.swift index 0c5cce08..55fa8545 100644 --- a/Meshtastic/Accessory/Protocols/Transport.swift +++ b/Meshtastic/Accessory/Protocols/Transport.swift @@ -9,7 +9,7 @@ import Foundation import CommonCrypto import SwiftUI -enum TransportType: String, CaseIterable { +enum TransportType: String, CaseIterable, Codable { case ble = "BLE" case tcp = "TCP" case serial = "Serial" @@ -53,14 +53,15 @@ protocol Transport { var requiresPeriodicHeartbeat: Bool { get } var supportsManualConnection: Bool { get } - func manuallyConnect(withConnectionString: String) async throws + func device(forManualConnection: String) -> Device? + func manuallyConnect(toDevice: Device) async throws } // Used to make stable-ish ID's for accessories that don't have a UUID extension String { - func toUUIDFormatHash() -> UUID? { + func toUUIDFormatHash() -> UUID { // Convert string to data - guard let data = self.data(using: .utf8) else { return nil } + let data = self.data(using: .utf8) ?? Data() // Create buffer for SHA-256 hash (32 bytes) var digest = [UInt8](repeating: 0, count: Int(CC_SHA256_DIGEST_LENGTH)) diff --git a/Meshtastic/Accessory/Transports/Bluetooth Low Energy/BLETransport.swift b/Meshtastic/Accessory/Transports/Bluetooth Low Energy/BLETransport.swift index ea4a22b8..aa1a32d4 100644 --- a/Meshtastic/Accessory/Transports/Bluetooth Low Energy/BLETransport.swift +++ b/Meshtastic/Accessory/Transports/Bluetooth Low Energy/BLETransport.swift @@ -403,7 +403,11 @@ class BLETransport: Transport { } - func manuallyConnect(withConnectionString: String) async throws { + func device(forManualConnection: String) -> Device? { + return nil + } + + func manuallyConnect(toDevice: Device) async throws { Logger.transport.error("🛜 [BLE] This transport does not support manual connections") } diff --git a/Meshtastic/Accessory/Transports/Serial/SerialTransport.swift b/Meshtastic/Accessory/Transports/Serial/SerialTransport.swift index 920dd538..b34b1884 100644 --- a/Meshtastic/Accessory/Transports/Serial/SerialTransport.swift +++ b/Meshtastic/Accessory/Transports/Serial/SerialTransport.swift @@ -31,7 +31,7 @@ class SerialTransport: Transport { while !Task.isCancelled { let ports = self.getSerialPorts() for port in ports { - let id = port.toUUIDFormatHash() ?? UUID() + let id = port.toUUIDFormatHash() if !portsAlreadyNotified.contains(port) { Logger.transport.info("🔱 [Serial] Port \(port, privacy: .public) found.") let newDevice = Device(id: id, @@ -45,9 +45,8 @@ class SerialTransport: Transport { for knownPort in portsAlreadyNotified where !ports.contains(knownPort) { // Previosuly seen port is no longer available Logger.transport.info("🔱 [Serial] Port \(knownPort, privacy: .public) is no longer connected.") - if let uuid = knownPort.toUUIDFormatHash() { - cont.yield(.deviceLost(uuid)) - } + let uuid = knownPort.toUUIDFormatHash() + cont.yield(.deviceLost(uuid)) portsAlreadyNotified.removeAll(where: {$0 == knownPort}) } try? await Task.sleep(for: .seconds(5)) @@ -118,7 +117,11 @@ class SerialTransport: Transport { return SerialConnection(path: device.identifier) } - func manuallyConnect(withConnectionString: String) async throws { + func device(forManualConnection: String) -> Device? { + return nil + } + + func manuallyConnect(toDevice: Device) async throws { Logger.transport.error("🔱 [USB] This transport does not support manual connections") } } diff --git a/Meshtastic/Accessory/Transports/TCP/TCPTransport.swift b/Meshtastic/Accessory/Transports/TCP/TCPTransport.swift index 787d257c..b7da7436 100644 --- a/Meshtastic/Accessory/Transports/TCP/TCPTransport.swift +++ b/Meshtastic/Accessory/Transports/TCP/TCPTransport.swift @@ -73,7 +73,7 @@ class TCPTransport: NSObject, Transport, NetServiceBrowserDelegate, NetServiceDe let ip = service.ipv4Address ?? "Unknown IP" // Use a mishmash of things and hash for stable? ID. - let idString = "\(service.name):\(host):\(ip):\(port)".toUUIDFormatHash() ?? UUID() + let idString = "\(service.name):\(host):\(ip):\(port)".toUUIDFormatHash() // Save the resolved service locally for later services[service.name] = ResolvedService(id: idString, service: service, host: host, port: port) @@ -97,8 +97,7 @@ class TCPTransport: NSObject, Transport, NetServiceBrowserDelegate, NetServiceDe let device = Device(id: idString, name: name, transportType: .tcp, - identifier: "\(host):\(port)", - connectionDetails: "\(ip):\(port)") + identifier: "\(host):\(port)") continuation?.yield(.deviceFound(device)) } @@ -151,16 +150,56 @@ class TCPTransport: NSObject, Transport, NetServiceBrowserDelegate, NetServiceDe } } - func manuallyConnect(withConnectionString: String) async throws { - let hashedIdentifier = withConnectionString.toUUIDFormatHash() ?? UUID() - let manualDevice = Device(id: hashedIdentifier, - name: "\(withConnectionString) (Manual)", - transportType: .tcp, identifier: withConnectionString) - try await AccessoryManager.shared.connect(to: manualDevice) + func device(forManualConnection connectionString: String) -> Device? { + let parts = connectionString.split(separator: ":") + var identifier: String + + switch parts.count { + case 1: + // host & default port + identifier = "\(parts[0]):4403" + + case 2: + // host & port + if parts[1].isValidTCPPort { + identifier = "\(parts[0]):\(parts[1])" + } + fallthrough + + default: + return nil + } + let hashedIdentifier = identifier.toUUIDFormatHash() + return Device(id: hashedIdentifier, + name: "\(connectionString) (Manual)", + transportType: .tcp, + identifier: connectionString, + isManualConnection: true) + } + + func manuallyConnect(toDevice device: Device) async throws { + try await AccessoryManager.shared.connect(to: device) } } +extension StringProtocol { + var isValidTCPPort: Bool { + // Check if the string is non-empty and contains only digits + guard !isEmpty, allSatisfy({ $0.isNumber }) else { + return false + } + + // Parse the string to an integer + guard let port = Int(self) else { + return false // Fails if the string can't be converted to an integer + } + + // Check if the port is in the valid TCP range (0–65535) + return port >= 0 && port <= 65535 + } +} + extension NetService { var ipv4Address: String? { for addressData in addresses ?? [] { diff --git a/Meshtastic/Extensions/Bundle.swift b/Meshtastic/Extensions/Bundle.swift index 0bb62acf..9f710bb7 100644 --- a/Meshtastic/Extensions/Bundle.swift +++ b/Meshtastic/Extensions/Bundle.swift @@ -23,4 +23,12 @@ extension Bundle { public var isTestFlight: Bool { return appStoreReceiptURL?.lastPathComponent == "sandboxReceipt" } + + public var isDebug: Bool { + #if DEBUG + return true + #else + return false + #endif + } } diff --git a/Meshtastic/Extensions/UserDefaults.swift b/Meshtastic/Extensions/UserDefaults.swift index d5a58e13..751ddc17 100644 --- a/Meshtastic/Extensions/UserDefaults.swift +++ b/Meshtastic/Extensions/UserDefaults.swift @@ -79,6 +79,7 @@ extension UserDefaults { case showDeviceOnboarding case usageDataAndCrashReporting case autoconnectOnDiscovery + case manualConnections case testIntEnum } @@ -178,6 +179,36 @@ extension UserDefaults { @UserDefault(.testIntEnum, defaultValue: .one) static var testIntEnum: TestIntEnum + + static var manualConnections: [Device] { + get { + // Retrieve data from UserDefaults + guard let data = UserDefaults.standard.data(forKey: Keys.manualConnections.rawValue) else { + return [] + } + + // Decode the Data back to [Device] + do { + let decoder = JSONDecoder() + let devices = try decoder.decode([Device].self, from: data) + return devices + } catch { + return [] + } + } + set { + do { + // Encode the [Device] to Data + let encoder = JSONEncoder() + let data = try encoder.encode(newValue) + + // Store the Data in UserDefaults + UserDefaults.standard.set(data, forKey: Keys.manualConnections.rawValue) + } catch { + print("Failed to encode manualConnections: \(error)") + } + } + } } enum TestIntEnum: Int, Decodable { diff --git a/Meshtastic/Views/Connect/Connect.swift b/Meshtastic/Views/Connect/Connect.swift index 7a264aa6..bb43ae04 100644 --- a/Meshtastic/Views/Connect/Connect.swift +++ b/Meshtastic/Views/Connect/Connect.swift @@ -26,8 +26,7 @@ struct Connect: View { @State var isUnsetRegion = false @State var invalidFirmwareVersion = false @State var liveActivityStarted = false - @State var presentingSwitchPreferredPeripheral = false - @State var selectedPeripherialId = "" + @ObservedObject var manualConnections = ManualConnectionList.shared var body: some View { NavigationStack { @@ -64,22 +63,10 @@ struct Connect: View { } Text("Connection Name").font(.callout)+Text(": \(connectedDevice.name.addingVariationSelectors)") .font(.callout).foregroundColor(Color.gray) - HStack { + HStack(alignment: .firstTextBaseline) { + TransportIcon(transportType: connectedDevice.transportType) if connectedDevice.transportType == .ble { - // baseline aligned looks better for the signal meter - HStack(alignment: .firstTextBaseline) { - TransportIcon(transportType: connectedDevice.transportType) - connectedDevice.getSignalStrength().map { SignalStrengthIndicator(signalStrength: $0, width: 5, height: 20) } - } - } else if connectedDevice.transportType == .tcp { - // Not baseline aligned looks better for the connection string - HStack { - TransportIcon(transportType: connectedDevice.transportType) - Text("\(connectedDevice.connectionDetails ?? connectedDevice.identifier)") - .foregroundColor(.gray) - } - } else { - TransportIcon(transportType: connectedDevice.transportType) + connectedDevice.getSignalStrength().map { SignalStrengthIndicator(signalStrength: $0, width: 5, height: 20) } } Spacer() } @@ -276,68 +263,33 @@ struct Connect: View { .textCase(nil) if !(accessoryManager.isConnected || accessoryManager .isConnecting) { - Section(header: HStack { - Text("Available Radios").font(.title) - Spacer() - ManualConnectionMenu() - }) { - ForEach(accessoryManager.devices.sorted(by: { $0.name < $1.name })) { device in - HStack { - if UserDefaults.preferredPeripheralId == device.id.uuidString { - Image(systemName: "star.fill") - .imageScale(.large).foregroundColor(.yellow) - .padding(.trailing) - } else { - Image(systemName: "circle.fill") - .imageScale(.large).foregroundColor(.gray) - .padding(.trailing) - } - VStack(alignment: .leading) { - Button(action: { - if UserDefaults.preferredPeripheralId.count > 0 && device.id.uuidString != UserDefaults.preferredPeripheralId { - if accessoryManager.allowDisconnect { - Task { try await accessoryManager.disconnect() } - } - presentingSwitchPreferredPeripheral = true - selectedPeripherialId = device.id.uuidString - } else { - Task { - try? await accessoryManager.connect(to: device) - } - } - }) { - Text(device.name).font(.callout) - } - // Show transport type - HStack { - TransportIcon(transportType: device.transportType) - if device.transportType == .tcp { - // Show IP and Port - Text("\(device.connectionDetails ?? device.identifier)") - .foregroundColor(.gray) - } - } - } - Spacer() - VStack { - device.getSignalStrength().map { SignalStrengthIndicator(signalStrength: $0) } - } - }.padding([.bottom, .top]) - } - } - .confirmationDialog("Connecting to a new radio will clear all app data on the phone.", isPresented: $presentingSwitchPreferredPeripheral, titleVisibility: .visible) { - Button("Connect to new radio?", role: .destructive) { - UserDefaults.preferredPeripheralId = selectedPeripherialId - UserDefaults.preferredPeripheralNum = 0 - if accessoryManager.allowDisconnect { - Task { try await accessoryManager.disconnect() } + Group { + Section(header: HStack { + Text("Available Radios").font(.title) + Spacer() + ManualConnectionMenu() + }) { + ForEach(accessoryManager.devices.sorted(by: { $0.name < $1.name })) { device in + DeviceConnectRow(device: device) } - clearCoreDataDatabase(context: context, includeRoutes: false) - clearNotifications() - if let radio = accessoryManager.devices.first(where: { $0.id.uuidString == selectedPeripherialId }) { - Task { - try await accessoryManager.connect(to: radio) + } + if manualConnections.connectionsList.count > 0 { + Section(header: Text("Manual Connections").font(.title)) { + ForEach(manualConnections.connectionsList) { device in + DeviceConnectRow(device: device) +#if targetEnvironment(macCatalyst) + .contextMenu { + Button { + manualConnections.remove(device: device) + } label: { + Label("Delete", systemImage: "trash") + } + } +#endif + }.onDelete { offsets in + manualConnections.remove(atOffsets: offsets) } + } } } @@ -491,6 +443,10 @@ struct TransportIcon: View { } struct ManualConnectionMenu: View { + + @EnvironmentObject var accessoryManager: AccessoryManager + @Environment(\.managedObjectContext) var context + private struct IterableTransport: Identifiable { let id: UUID let icon: Image @@ -509,7 +465,9 @@ struct ManualConnectionMenu: View { @State private var selectedTransport: IterableTransport? @State private var showAlert: Bool = false @State private var connectionString = "" - + @State var presentingSwitchPreferredPeripheral = false + @State var deviceForManualConnection: Device? + var body: some View { Menu { ForEach(transports) { transport in @@ -539,11 +497,114 @@ struct ManualConnectionMenu: View { Button("OK", action: { if !connectionString.isEmpty { - Task { - try await selectedTransport.transport.manuallyConnect(withConnectionString: connectionString) + if let device = selectedTransport.transport.device(forManualConnection: connectionString) { + if UserDefaults.preferredPeripheralId == device.id.uuidString { + Task { + try await selectedTransport.transport.manuallyConnect(toDevice: device) + } + } else { + deviceForManualConnection = device + presentingSwitchPreferredPeripheral = true + } } } }) - } + }.confirmationDialog("Connecting to a new radio will clear all app data on the phone.", isPresented: $presentingSwitchPreferredPeripheral, titleVisibility: .visible) { + Button("Connect to new radio?", role: .destructive) { + if let device = deviceForManualConnection { + UserDefaults.preferredPeripheralId = device.id.uuidString + UserDefaults.preferredPeripheralNum = 0 + if accessoryManager.allowDisconnect { + Task { try await accessoryManager.disconnect() } + } + clearCoreDataDatabase(context: context, includeRoutes: false) + clearNotifications() + Task { + try await selectedTransport?.transport.manuallyConnect(toDevice: device) + } + + // Clean up just in case + deviceForManualConnection = nil + } + } + } } } + +struct DeviceConnectRow: View { + @Environment(\.managedObjectContext) var context + @EnvironmentObject var accessoryManager: AccessoryManager + @State var presentingSwitchPreferredPeripheral = false + let device: Device + + var body: some View { + HStack { + if UserDefaults.preferredPeripheralId == device.id.uuidString { + Image(systemName: "star.fill") + .imageScale(.large).foregroundColor(.yellow) + .padding(.trailing) + } else { + Image(systemName: "circle.fill") + .imageScale(.large).foregroundColor(.gray) + .padding(.trailing) + } + VStack(alignment: .leading) { + Button(action: { + if UserDefaults.preferredPeripheralId.count > 0 && device.id.uuidString != UserDefaults.preferredPeripheralId { + if accessoryManager.allowDisconnect { + Task { try await accessoryManager.disconnect() } + } + presentingSwitchPreferredPeripheral = true + } else { + Task { + try? await accessoryManager.connect(to: device) + } + } + }) { + Text(device.name).font(.callout) + } + // Show transport type +#if !targetEnvironment(macCatalyst) + HStack(alignment: .center){ + TransportIcon(transportType: device.transportType) + if device.isManualConnection && (device.longName != nil || device.shortName != nil) { + VStack (alignment: .leading) { + Text("Last seen device:") + Text("\(String(describing: device))") + } + } + }.padding(.top, 3.0) +#else + //Different alignment for Mac + HStack(alignment: .firstTextBaseline){ + TransportIcon(transportType: device.transportType) + if device.isManualConnection && (device.longName != nil || device.shortName != nil) { + Text("Last seen device: \(String(describing: device))") + } + } +#endif + } + Spacer() + VStack { + device.getSignalStrength().map { + SignalStrengthIndicator(signalStrength: $0) + } + } + }.padding([.bottom, .top]) + .confirmationDialog("Connecting to a new radio will clear all app data on the phone.", isPresented: $presentingSwitchPreferredPeripheral, titleVisibility: .visible) { + Button("Connect to new radio?", role: .destructive) { + UserDefaults.preferredPeripheralId = device.id.uuidString + UserDefaults.preferredPeripheralNum = 0 + if accessoryManager.allowDisconnect { + Task { try await accessoryManager.disconnect() } + } + clearCoreDataDatabase(context: context, includeRoutes: false) + clearNotifications() + Task { + try await accessoryManager.connect(to: device) + } + } + } + } +} + From 7668a7a7ae9eeb35ace627553841ff56155eb246 Mon Sep 17 00:00:00 2001 From: Benjamin Faershtein <119711889+RCGV1@users.noreply.github.com> Date: Tue, 28 Oct 2025 06:19:12 -0700 Subject: [PATCH 15/68] Show who relayed messages (#1486) * Add identification for node that relayed text messages and add count for ammount of relayers of your message * Ack Relays --- Localizable.xcstrings | 18 +++++++ .../CoreData/MessageEntityExtension.swift | 35 +++++++++++++ Meshtastic/Helpers/MeshPackets.swift | 7 +++ .../contents | 4 +- .../Messages/MessageContextMenuItems.swift | 50 ++++++++++++++----- 5 files changed, 100 insertions(+), 14 deletions(-) diff --git a/Localizable.xcstrings b/Localizable.xcstrings index 6f4a50b6..44aa74bb 100644 --- a/Localizable.xcstrings +++ b/Localizable.xcstrings @@ -28480,6 +28480,7 @@ } }, "Received Ack" : { + "extractionState" : "stale", "localizations" : { "de" : { "stringUnit" : { @@ -28542,8 +28543,12 @@ } } } + }, + "Received Ack: %@" : { + }, "Recipient Ack" : { + "extractionState" : "stale", "localizations" : { "de" : { "stringUnit" : { @@ -28606,6 +28611,9 @@ } } } + }, + "Recipient Ack: %@" : { + }, "Recording route" : { "localizations" : { @@ -28789,6 +28797,16 @@ } } }, + "Relayed by %d %@" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "Relayed by %1$d %2$@" + } + } + } + }, "Release Notes" : { "localizations" : { "it" : { diff --git a/Meshtastic/Extensions/CoreData/MessageEntityExtension.swift b/Meshtastic/Extensions/CoreData/MessageEntityExtension.swift index e7abb191..5c06ecf2 100644 --- a/Meshtastic/Extensions/CoreData/MessageEntityExtension.swift +++ b/Meshtastic/Extensions/CoreData/MessageEntityExtension.swift @@ -38,4 +38,39 @@ extension MessageEntity { } return false // First message will have no timestamp } + + func relayDisplay() -> String? { + + guard self.relayNode != 0 else { return nil } + let context = PersistenceController.shared.container.viewContext + + let relaySuffix = Int64(self.relayNode & 0xFF) + let request: NSFetchRequest = UserEntity.fetchRequest() + request.predicate = NSPredicate(format: "(num & 0xFF) == %lld", relaySuffix) + + do { + let users = try context.fetch(request) + + // If exactly one match is found, return its name + if users.count == 1, let name = users.first?.longName, !name.isEmpty { + return "\(name)" + } + + // If no exact match, find the node with the smallest hopsAway + if let closestNode = users.min(by: { lhs, rhs in + guard let lhsHops = lhs.userNode?.hopsAway, let rhsHops = rhs.userNode?.hopsAway else { + return false + } + return lhsHops < rhsHops + }), let name = closestNode.longName, !name.isEmpty { + return "\(name)" + } + + // Fallback to hex node number if no matches + return String(format: "Node 0x%02X", UInt32(self.relayNode & 0xFF)) + + } catch { + return String(format: "Node 0x%02X", UInt32(self.relayNode & 0xFF)) + } + } } diff --git a/Meshtastic/Helpers/MeshPackets.swift b/Meshtastic/Helpers/MeshPackets.swift index e3aa3252..1e68896b 100644 --- a/Meshtastic/Helpers/MeshPackets.swift +++ b/Meshtastic/Helpers/MeshPackets.swift @@ -624,6 +624,7 @@ func adminResponseAck (packet: MeshPacket, context: NSManagedObjectContext) { fetchedMessage[0].ackError = Int32(RoutingError.none.rawValue) fetchedMessage[0].receivedACK = true fetchedMessage[0].realACK = true + fetchedMessage[0].relayNode = Int64(packet.relayNode) fetchedMessage[0].ackSNR = packet.rxSnr if fetchedMessage[0].fromUser != nil { fetchedMessage[0].fromUser?.objectWillChange.send() @@ -699,9 +700,11 @@ func routingPacket (packet: MeshPacket, connectedNodeNum: Int64, context: NSMana fetchedMessage[0].realACK = true } } + fetchedMessage[0].relayNode = Int64(packet.relayNode) fetchedMessage[0].ackError = Int32(routingMessage.errorReason.rawValue) if routingMessage.errorReason == Routing.Error.none { fetchedMessage[0].receivedACK = true + fetchedMessage[0].relays += 1 } fetchedMessage[0].ackSNR = packet.rxSnr @@ -944,6 +947,9 @@ func textMessageAppPacket( } else { newMessage.messageTimestamp = Int32(Date().timeIntervalSince1970) } + if packet.relayNode != 0 { + newMessage.relayNode = Int64(packet.relayNode) + } newMessage.receivedACK = false newMessage.snr = packet.rxSnr newMessage.rssi = packet.rxRssi @@ -983,6 +989,7 @@ func textMessageAppPacket( newMessage.pkiEncrypted = true newMessage.publicKey = packet.publicKey } + /// Check for key mismatch if let nodeKey = newMessage.fromUser?.publicKey { if newMessage.toUser != nil && packet.pkiEncrypted && !packet.publicKey.isEmpty { diff --git a/Meshtastic/Meshtastic.xcdatamodeld/MeshtasticDataModelV 55.xcdatamodel/contents b/Meshtastic/Meshtastic.xcdatamodeld/MeshtasticDataModelV 55.xcdatamodel/contents index b5e4a81e..a6e5465f 100644 --- a/Meshtastic/Meshtastic.xcdatamodeld/MeshtasticDataModelV 55.xcdatamodel/contents +++ b/Meshtastic/Meshtastic.xcdatamodeld/MeshtasticDataModelV 55.xcdatamodel/contents @@ -1,5 +1,5 @@ - + @@ -164,6 +164,8 @@ + + diff --git a/Meshtastic/Views/Messages/MessageContextMenuItems.swift b/Meshtastic/Views/Messages/MessageContextMenuItems.swift index 33554de7..63104320 100644 --- a/Meshtastic/Views/Messages/MessageContextMenuItems.swift +++ b/Meshtastic/Views/Messages/MessageContextMenuItems.swift @@ -11,6 +11,7 @@ struct MessageContextMenuItems: View { let isCurrentUser: Bool @Binding var isShowingDeleteConfirmation: Bool let onReply: () -> Void + @State var relayDisplay: String? = nil var body: some View { VStack { @@ -19,6 +20,14 @@ struct MessageContextMenuItems: View { } Text("Channel") + Text(": \(message.channel)") } + .onAppear { + DispatchQueue.global(qos: .userInitiated).async { + let result = message.relayDisplay() + DispatchQueue.main.async { + relayDisplay = result + } + } + } Menu("Tapback") { ForEach(Tapbacks.allCases) { tb in @@ -59,12 +68,27 @@ struct MessageContextMenuItems: View { } Menu("Message Details") { + // Precompute values to avoid executing non-View code inside the ViewBuilder + let messageDate = Date(timeIntervalSince1970: TimeInterval(message.messageTimestamp)) + let ackDate = Date(timeIntervalSince1970: TimeInterval(message.ackTimestamp)) + let sixMonthsAgo = Calendar.current.date(byAdding: .month, value: -6, to: Date()) + + // Compute a relay display string if relayNode is present + + VStack { - let messageDate = Date(timeIntervalSince1970: TimeInterval(message.messageTimestamp)) - Text("\(messageDate.formattedDate(format: MessageText.dateFormatString))").foregroundColor(.gray) + Text("\(messageDate.formattedDate(format: MessageText.dateFormatString))") + .foregroundColor(.gray) } - if !isCurrentUser && !(message.fromUser?.userNode?.viaMqtt ?? false) && message.fromUser?.userNode?.hopsAway ?? -1 == 0 { + if let relayDisplay { + let prefix = message.realACK ? "Ack Relay: " : "Relay: " + Text(prefix + relayDisplay) + .foregroundColor(relayDisplay.contains("Node ") ? .gray : .primary) + .font(relayDisplay.contains("Node ") ? .caption : .body) + } + + if !isCurrentUser && !(message.fromUser?.userNode?.viaMqtt ?? false) && message.fromUser?.userNode?.hopsAway ?? -1 == 0 { VStack { Text("SNR \(String(format: "%.2f", message.snr)) dB") Text("RSSI \(String(format: "%.2f", message.rssi)) dBm") @@ -74,29 +98,29 @@ struct MessageContextMenuItems: View { Text("Hops Away \(message.fromUser?.userNode?.hopsAway ?? 0)") } } + if message.relays != 0 && message.realACK == false { + Text("Relayed by \(message.relays) \(message.relays == 1 ? "node" : "nodes")") + } if isCurrentUser && message.receivedACK { VStack { - Text("Received Ack") + Text(": \(message.receivedACK ? "✔️" : "")") - Text("Recipient Ack") + Text(": \(message.realACK ? "✔️" : "")") + Text("Received Ack: \(message.receivedACK ? "✔️" : "")") + Text("Recipient Ack: \(message.realACK ? "✔️" : "")") } } else if isCurrentUser && message.ackError == 0 { - // Empty Error Text("Waiting") } else if isCurrentUser && message.ackError > 0 { let ackErrorVal = RoutingError(rawValue: Int(message.ackError)) Text("\(ackErrorVal?.display ?? "Empty Ack Error")") .fixedSize(horizontal: false, vertical: true) } + if isCurrentUser { - VStack { - let ackDate = Date(timeIntervalSince1970: TimeInterval(message.ackTimestamp)) - let sixMonthsAgo = Calendar.current.date(byAdding: .month, value: -6, to: Date()) - if ackDate >= sixMonthsAgo! { - Text("Ack Time: \(ackDate.formattedDate(format: MessageText.timeFormatString))") - .foregroundColor(.gray) - } + if let sixMonthsAgo, ackDate >= sixMonthsAgo { + Text("Ack Time: \(ackDate.formattedDate(format: MessageText.timeFormatString))") + .foregroundColor(.gray) } } + if message.ackSNR != 0 { VStack { Text("Ack SNR: \(String(format: "%.2f", message.ackSNR)) dB") From e7b35838c3e9ea556c636d753115c9eee8b3a514 Mon Sep 17 00:00:00 2001 From: Mike Robbins Date: Tue, 28 Oct 2025 09:20:31 -0400 Subject: [PATCH 16/68] upsertPositionPacket: don't use future timestamps to set node's lastHeard (#1488) --- Meshtastic/Persistence/UpdateCoreData.swift | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/Meshtastic/Persistence/UpdateCoreData.swift b/Meshtastic/Persistence/UpdateCoreData.swift index 3f758329..ad9116b9 100644 --- a/Meshtastic/Persistence/UpdateCoreData.swift +++ b/Meshtastic/Persistence/UpdateCoreData.swift @@ -465,13 +465,17 @@ func upsertPositionPacket (packet: MeshPacket, context: NSManagedObjectContext) mutablePositions.add(position) fetchedNode[0].id = Int64(packet.from) fetchedNode[0].num = Int64(packet.from) - if positionMessage.time > 0 { + + // Update the node's lastHeard. + // Some misconfigured nodes will broadcast position packets that claim GPS timestamps in the future. When updating lastHeard, don't use any future timestamps: fallback to using rxTime or Date() instead. + if positionMessage.time > 0 && (Date(timeIntervalSince1970: TimeInterval(Int64(positionMessage.time))) <= Date()) { fetchedNode[0].lastHeard = Date(timeIntervalSince1970: TimeInterval(Int64(positionMessage.time))) } else if packet.rxTime > 0 { fetchedNode[0].lastHeard = Date(timeIntervalSince1970: TimeInterval(Int64(packet.rxTime))) } else { fetchedNode[0].lastHeard = Date() } + fetchedNode[0].snr = packet.rxSnr fetchedNode[0].rssi = packet.rxRssi fetchedNode[0].viaMqtt = packet.viaMqtt From 92b16466655ea3ac48bf459a561e3adace4b0269 Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Tue, 28 Oct 2025 07:02:33 -0700 Subject: [PATCH 17/68] R1 NEO --- .../MUZIR1NEO.imageset/Contents.json | 12 ++ .../MUZIR1NEO.imageset/muzi_r1_neo.svg | 141 ++++++++++++++++++ 2 files changed, 153 insertions(+) create mode 100644 Meshtastic/Assets.xcassets/MUZIR1NEO.imageset/Contents.json create mode 100644 Meshtastic/Assets.xcassets/MUZIR1NEO.imageset/muzi_r1_neo.svg diff --git a/Meshtastic/Assets.xcassets/MUZIR1NEO.imageset/Contents.json b/Meshtastic/Assets.xcassets/MUZIR1NEO.imageset/Contents.json new file mode 100644 index 00000000..ced0ad0c --- /dev/null +++ b/Meshtastic/Assets.xcassets/MUZIR1NEO.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "muzi_r1_neo.svg", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Meshtastic/Assets.xcassets/MUZIR1NEO.imageset/muzi_r1_neo.svg b/Meshtastic/Assets.xcassets/MUZIR1NEO.imageset/muzi_r1_neo.svg new file mode 100644 index 00000000..2f2cb0bf --- /dev/null +++ b/Meshtastic/Assets.xcassets/MUZIR1NEO.imageset/muzi_r1_neo.svg @@ -0,0 +1,141 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + From 12a1ca1bc5f76350b10b1072afa210aef447d541 Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Tue, 28 Oct 2025 07:03:04 -0700 Subject: [PATCH 18/68] Neo --- Meshtastic/Extensions/CoreData/UserEntityExtension.swift | 3 +++ Meshtastic/Resources/DeviceHardware.json | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/Meshtastic/Extensions/CoreData/UserEntityExtension.swift b/Meshtastic/Extensions/CoreData/UserEntityExtension.swift index 48e44e30..830ac772 100644 --- a/Meshtastic/Extensions/CoreData/UserEntityExtension.swift +++ b/Meshtastic/Extensions/CoreData/UserEntityExtension.swift @@ -134,6 +134,9 @@ extension UserEntity { return "NANOG1" case "NANOG2ULTRA": return "NANOG2ULTRA" + /// Muzi Works + case "MUZIR1NEO": + return "MUZIR1NEO" case "STATIONG2": return "STATIONG2" /// DIY Devices diff --git a/Meshtastic/Resources/DeviceHardware.json b/Meshtastic/Resources/DeviceHardware.json index 1306cfd2..39b61693 100644 --- a/Meshtastic/Resources/DeviceHardware.json +++ b/Meshtastic/Resources/DeviceHardware.json @@ -1051,7 +1051,7 @@ "hwModelSlug": "MUZI_R1_NEO", "platformioTarget": "r1-neo", "architecture": "nrf52840", - "activelySupported": false, + "activelySupported": true, "supportLevel": 1, "displayName": "muzi R1 Neo", "tags": [ From 58b1204f39c79f88816070a53f67ec8049ecdddb Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Tue, 28 Oct 2025 07:36:18 -0700 Subject: [PATCH 19/68] Update Meshtastic/Views/Settings/AppSettings.swift Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- Meshtastic/Views/Settings/AppSettings.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Meshtastic/Views/Settings/AppSettings.swift b/Meshtastic/Views/Settings/AppSettings.swift index 6a809b42..2f10c2af 100644 --- a/Meshtastic/Views/Settings/AppSettings.swift +++ b/Meshtastic/Views/Settings/AppSettings.swift @@ -57,7 +57,7 @@ struct AppSettings: View { .tint(.accentColor) } #if targetEnvironment(macCatalyst) - + // App Icon Picker is disabled on macOS Catalyst #else Button { isPresentingAppIconSheet.toggle() From 3b9c0bf53ed1053b541fba0871176448ee6716ee Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Tue, 28 Oct 2025 08:58:00 -0700 Subject: [PATCH 20/68] Remove bad if --- Meshtastic/MeshtasticApp.swift | 2 -- 1 file changed, 2 deletions(-) diff --git a/Meshtastic/MeshtasticApp.swift b/Meshtastic/MeshtasticApp.swift index fccf9a7c..466ed67a 100644 --- a/Meshtastic/MeshtasticApp.swift +++ b/Meshtastic/MeshtasticApp.swift @@ -43,7 +43,6 @@ struct MeshtasticAppleApp: App { } #endif -#if false Datadog.initialize( with: Datadog.Configuration( clientToken: clientToken, @@ -80,7 +79,6 @@ struct MeshtasticAppleApp: App { ) ) } -#endif accessoryManager = AccessoryManager.shared accessoryManager.appState = appState From 247ec497a7cc24629be37540488acc1331779157 Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Tue, 28 Oct 2025 14:34:39 -0700 Subject: [PATCH 21/68] Git rid of extra environment variable --- Localizable.xcstrings | 5 ++--- Meshtastic/MeshtasticApp.swift | 2 -- Meshtastic/Views/Messages/ChannelMessageList.swift | 3 +-- Meshtastic/Views/Messages/ChannelMessageRow.swift | 4 ++-- Meshtastic/Views/Messages/UserMessageList.swift | 3 +-- Meshtastic/Views/Messages/UserMessageRow.swift | 4 ++-- Meshtastic/Views/Nodes/Helpers/Map/PositionPopover.swift | 4 ++-- 7 files changed, 10 insertions(+), 15 deletions(-) diff --git a/Localizable.xcstrings b/Localizable.xcstrings index 44aa74bb..c725645e 100644 --- a/Localizable.xcstrings +++ b/Localizable.xcstrings @@ -20936,9 +20936,8 @@ } } }, - "Meshtastic does not collect any personal information. We do anonymously collect usage and crash data to improve the app. This helps us understand how the app is being used and where we can make improvements. The data we collect is non-personally identifiable and cannot be linked to you as an individual. You can opt out of this under app settings." : { - "comment" : "Privacy policy text for Meshtastic.", - "isCommentAutoGenerated" : true + "Meshtastic does not collect any personal information. We do anonymously collect usage and crash data to improve the app. You can opt out under app settings." : { + }, "Meshtastic Node %@ has shared channels with you" : { "localizations" : { diff --git a/Meshtastic/MeshtasticApp.swift b/Meshtastic/MeshtasticApp.swift index 466ed67a..c773d78c 100644 --- a/Meshtastic/MeshtasticApp.swift +++ b/Meshtastic/MeshtasticApp.swift @@ -210,7 +210,5 @@ struct MeshtasticAppleApp: App { .environment(\.managedObjectContext, persistenceController.container.viewContext) .environmentObject(appState) .environmentObject(accessoryManager) - .environmentObject(appState.router) } - } diff --git a/Meshtastic/Views/Messages/ChannelMessageList.swift b/Meshtastic/Views/Messages/ChannelMessageList.swift index ba0bc701..be3959d2 100644 --- a/Meshtastic/Views/Messages/ChannelMessageList.swift +++ b/Meshtastic/Views/Messages/ChannelMessageList.swift @@ -12,7 +12,6 @@ import SwiftUI struct ChannelMessageList: View { @EnvironmentObject var appState: AppState - @EnvironmentObject var router: Router @Environment(\.scenePhase) var scenePhase @Environment(\.managedObjectContext) var context @EnvironmentObject var accessoryManager: AccessoryManager @@ -61,7 +60,7 @@ struct ChannelMessageList: View { } private func routerIsShowingThisChannel() -> Bool { - guard router.navigationState.selectedTab == .messages else { return false } + guard appState.router.navigationState.selectedTab == .messages else { return false } return scenePhase == .active } diff --git a/Meshtastic/Views/Messages/ChannelMessageRow.swift b/Meshtastic/Views/Messages/ChannelMessageRow.swift index 9e3c8124..eb0f2a9f 100644 --- a/Meshtastic/Views/Messages/ChannelMessageRow.swift +++ b/Meshtastic/Views/Messages/ChannelMessageRow.swift @@ -3,7 +3,7 @@ import MeshtasticProtobufs import SwiftUI struct ChannelMessageRow: View { - @EnvironmentObject var router: Router + @EnvironmentObject var appState: AppState // Core Data object observed for changes (like Tapbacks being received) @ObservedObject var message: MessageEntity @@ -98,7 +98,7 @@ struct ChannelMessageRow: View { CircleText(text: message.fromUser?.shortName ?? "?", color: Color(UIColor(hex: UInt32(message.fromUser?.num ?? 0))), circleSize: 50) .onTapGesture(count: 2) { if let nodeNum = message.fromUser?.num { - router.navigateToNodeDetail(nodeNum: Int64(nodeNum)) + appState.router.navigateToNodeDetail(nodeNum: Int64(nodeNum)) } } .padding(.all, 5).offset(y: -7) diff --git a/Meshtastic/Views/Messages/UserMessageList.swift b/Meshtastic/Views/Messages/UserMessageList.swift index 394431d5..9a3425bc 100644 --- a/Meshtastic/Views/Messages/UserMessageList.swift +++ b/Meshtastic/Views/Messages/UserMessageList.swift @@ -12,7 +12,6 @@ import MeshtasticProtobufs // Added to ensure RoutingError is accessible if need struct UserMessageList: View { @EnvironmentObject var appState: AppState - @EnvironmentObject var router: Router @EnvironmentObject var accessoryManager: AccessoryManager @Environment(\.scenePhase) var scenePhase @Environment(\.managedObjectContext) var context @@ -58,7 +57,7 @@ struct UserMessageList: View { } private func routerIsShowingThisUser() -> Bool { - guard router.navigationState.selectedTab == .messages else { return false } + guard appState.router.navigationState.selectedTab == .messages else { return false } return scenePhase == .active } diff --git a/Meshtastic/Views/Messages/UserMessageRow.swift b/Meshtastic/Views/Messages/UserMessageRow.swift index 00579fb3..d469462b 100644 --- a/Meshtastic/Views/Messages/UserMessageRow.swift +++ b/Meshtastic/Views/Messages/UserMessageRow.swift @@ -11,7 +11,7 @@ import SwiftUI struct UserMessageRow: View { - @EnvironmentObject var router: Router + @EnvironmentObject var appState: AppState @ObservedObject var message: MessageEntity let allMessages: [MessageEntity] let previousMessage: MessageEntity? @@ -105,7 +105,7 @@ struct UserMessageRow: View { CircleText(text: message.fromUser?.shortName ?? "?", color: Color(UIColor(hex: UInt32(message.fromUser?.num ?? 0))), circleSize: 50) .onTapGesture(count: 2) { if let nodeNum = message.fromUser?.num { - router.navigateToNodeDetail(nodeNum: Int64(nodeNum)) + appState.router.navigateToNodeDetail(nodeNum: Int64(nodeNum)) } } .padding(.all, 5).offset(y: -7) diff --git a/Meshtastic/Views/Nodes/Helpers/Map/PositionPopover.swift b/Meshtastic/Views/Nodes/Helpers/Map/PositionPopover.swift index e9d11aa4..2e5e2809 100644 --- a/Meshtastic/Views/Nodes/Helpers/Map/PositionPopover.swift +++ b/Meshtastic/Views/Nodes/Helpers/Map/PositionPopover.swift @@ -12,7 +12,7 @@ struct PositionPopover: View { @ObservedObject var locationsHandler = LocationsHandler.shared @Environment(\.managedObjectContext) var context - @EnvironmentObject var router: Router + @EnvironmentObject var appState: AppState private var idiom: UIUserInterfaceIdiom { UIDevice.current.userInterfaceIdiom } @Environment(\.dismiss) private var dismiss @Environment(\.openURL) var openURL @@ -29,7 +29,7 @@ struct PositionPopover: View { ZStack { Button { if let nodeNum = position.nodePosition?.num { - router.navigateToNodeDetail(nodeNum: Int64(nodeNum)) + appState.router.navigateToNodeDetail(nodeNum: Int64(nodeNum)) dismiss() } } label: { From 59d106ac1e0ecd5e4f1b2f5d670e6e0a8a371015 Mon Sep 17 00:00:00 2001 From: jake-b <1012393+jake-b@users.noreply.github.com> Date: Wed, 29 Oct 2025 17:54:16 -0400 Subject: [PATCH 22/68] Update Meshtastic/Accessory/Transports/TCP/TCPTransport.swift Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- Meshtastic/Accessory/Transports/TCP/TCPTransport.swift | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Meshtastic/Accessory/Transports/TCP/TCPTransport.swift b/Meshtastic/Accessory/Transports/TCP/TCPTransport.swift index b7da7436..1acc988c 100644 --- a/Meshtastic/Accessory/Transports/TCP/TCPTransport.swift +++ b/Meshtastic/Accessory/Transports/TCP/TCPTransport.swift @@ -163,8 +163,9 @@ class TCPTransport: NSObject, Transport, NetServiceBrowserDelegate, NetServiceDe // host & port if parts[1].isValidTCPPort { identifier = "\(parts[0]):\(parts[1])" + } else { + return nil } - fallthrough default: return nil From 8df71404b3ac7fdc412c4c3f4fcae64fd75159da Mon Sep 17 00:00:00 2001 From: Mike Robbins Date: Thu, 30 Oct 2025 17:15:18 -0400 Subject: [PATCH 23/68] MeshMap performance: quick wins (#1490) * MeshMap: change onMapCameraChange frequency to .onEnd so that zooming doesn't cause continuous SwiftUI reevaluation on every frame * MeshMapContent: factor out reducedPrecisionMapCircles into a separate function * MeshMapContent: when multiple reducedPrecisionCircles have the same (lat,lon,radius), just draw one (big perf boost in dense areas) --- .../Map/MapContent/MeshMapContent.swift | 61 +++++++++++++++---- Meshtastic/Views/Nodes/MeshMap.swift | 3 +- 2 files changed, 51 insertions(+), 13 deletions(-) diff --git a/Meshtastic/Views/Nodes/Helpers/Map/MapContent/MeshMapContent.swift b/Meshtastic/Views/Nodes/Helpers/Map/MapContent/MeshMapContent.swift index f1fd931f..480a5cba 100644 --- a/Meshtastic/Views/Nodes/Helpers/Map/MapContent/MeshMapContent.swift +++ b/Meshtastic/Views/Nodes/Helpers/Map/MapContent/MeshMapContent.swift @@ -15,6 +15,12 @@ struct IdentifiableOverlay: Identifiable { var id: ObjectIdentifier { ObjectIdentifier(overlay as AnyObject) } } +struct ReducedPrecisionMapCircleKey: Hashable { + let latitudeI: Int32 + let longitudeI: Int32 + let precisionBits: Int32 +} + struct MeshMapContent: MapContent { /// Parameters @@ -43,7 +49,7 @@ struct MeshMapContent: MapContent { @FetchRequest(sortDescriptors: [NSSortDescriptor(key: "name", ascending: true)], predicate: NSPredicate(format: "enabled == true", ""), animation: .none) private var routes: FetchedResults - + @MapContentBuilder var positionAnnotations: some MapContent { ForEach(positions, id: \.id) { position in @@ -60,16 +66,7 @@ struct MeshMapContent: MapContent { let nodeColor = UIColor(hex: UInt32(position.nodePosition?.num ?? 0)) let positionName = position.nodePosition?.user?.longName ?? "?" - /// Reduced Precision Map Circle - if 12...15 ~= position.precisionBits { - let pp = PositionPrecision(rawValue: Int(position.precisionBits)) - let radius: CLLocationDistance = pp?.precisionMeters ?? 0 - if radius > 0.0 { - MapCircle(center: position.coordinate, radius: radius) - .foregroundStyle(Color(nodeColor).opacity(0.25)) - .stroke(.white, lineWidth: 1) - } - } + // Use a hash of the position ID to stagger animation delays for each node, preventing synchronized animations and improving visual distinction. let calculatedDelay = Double(position.id.hashValue % 100) / 100.0 * 0.5 @@ -91,7 +88,46 @@ struct MeshMapContent: MapContent { } } } - + + private var reducedPrecisionCircleItems: [(nodeNum: Int64, circleKey: ReducedPrecisionMapCircleKey)] { + // Precompute *unique* reduced-precision circles so we don't have to redraw tons of identical (center, radius) circles in dense map areas. (Since they're all transparent, this causes severe FPS drop when zoomed into areas where there are a ton of overlapping circles.) + var lowestNumForKey: [ReducedPrecisionMapCircleKey: Int64] = [:] + // Populate a dict where the key is (lat, lon, bits) and the value is the *lowest* node.num seen for that key. + // That lowest node.num value is used to create a stable color for the MapCircle and stable id for ForEach. + for position in positions { + // Same filter criteria as positionAnnotations: + if (!showFavorites || (position.nodePosition?.favorite == true)) && !(position.nodePosition?.ignored == true) { + if 12...15 ~= position.precisionBits { + let nodeNum = position.nodePosition?.num ?? 0 + let key = ReducedPrecisionMapCircleKey(latitudeI: position.latitudeI, longitudeI: position.longitudeI, precisionBits: position.precisionBits) + if let existing = lowestNumForKey[key] { + if nodeNum < existing { lowestNumForKey[key] = nodeNum } + } else { + lowestNumForKey[key] = nodeNum + } + } + } + } + // Sort by nodeNum just to keep draw order stable. + return lowestNumForKey.map { ($0.value, $0.key) }.sorted { $0.nodeNum < $1.nodeNum } + } + + @MapContentBuilder + var reducedPrecisionMapCircles: some MapContent { + ForEach(reducedPrecisionCircleItems, id: \.nodeNum) { item in + let circleKey = item.circleKey + let nodeNum = item.nodeNum + let radius = PositionPrecision(rawValue: Int(circleKey.precisionBits))?.precisionMeters ?? 0 + if radius > 0.0 { + let center = CLLocationCoordinate2D(latitude: Double(circleKey.latitudeI) / 1e7, longitude: Double(circleKey.longitudeI) / 1e7) + let nodeColor = UIColor(hex: UInt32(nodeNum)) + MapCircle(center: center, radius: radius) + .foregroundStyle(Color(nodeColor).opacity(0.25)) + .stroke(.white, lineWidth: 1) + } + } + } + @MapContentBuilder var routeAnnotations: some MapContent { ForEach(routes) { route in @@ -167,6 +203,7 @@ struct MeshMapContent: MapContent { } positionAnnotations + reducedPrecisionMapCircles routeAnnotations waypointAnnotations } diff --git a/Meshtastic/Views/Nodes/MeshMap.swift b/Meshtastic/Views/Nodes/MeshMap.swift index 9d8911ab..d72987da 100644 --- a/Meshtastic/Views/Nodes/MeshMap.swift +++ b/Meshtastic/Views/Nodes/MeshMap.swift @@ -75,7 +75,8 @@ struct MeshMap: View { } .controlSize(.regular) .offset(y: 100) - .onMapCameraChange(frequency: MapCameraUpdateFrequency.continuous, { context in + .onMapCameraChange(frequency: MapCameraUpdateFrequency.onEnd, { context in + // distance is only used for long-press waypoint creation, so we don't need continuous updates which touch @State and force rerenders as we pan and (for distance in particular) zoom around the map. onEnd is more than enough. distance = context.camera.distance }) .onTapGesture(count: 1, perform: { position in From 402cb836b5dc271b68e421d660eaf29cc9439ae8 Mon Sep 17 00:00:00 2001 From: Mike Robbins Date: Thu, 30 Oct 2025 17:32:27 -0400 Subject: [PATCH 24/68] NodeMap performance improvements for high # positions history (#1480) * NodeMapContent: move Route Lines out of ForEach * NodeMapContent: move Convex Hull out of ForEach * NodeMapContent: Replace `position.nodePosition?` with `node` * NodeMapContent: drop unnecessary LazyVStack in showNodeHistory * NodeMapContent: hoist out nodeColorSwift * Move lineCoords, loraCoords calculations within showRouteLines, showConvexHull respectively * Hoist out repeated node.metadata?.positionFlags lookups / PositionFlags creation * NodeMapContent: remove unused @State * NodeMapSwiftUI: add NodeMapContentEquatableWrapper and NodeMapContentSignature to prevent frequent NodeMapContent recomputation and infinite render loops * NodeMapSwiftUI: disable animation during SwiftUI transactions * NodeMapContent: hoist nodeBorderColor and set allowsHitTesting(false) on history point views * NodeMapContent: prerenderHistoryPointCircle and prerenderHistoryPointArrow to avoid thousands of vector draw operations * NodeMapContent: Shared coordinate list for Route Lines and Convex Hull * NodeMapContent.prerenderHistoryPointArrow: add .frame(width: 16, height: 16) --- .../Map/MapContent/NodeMapContent.swift | 146 ++++++++++-------- .../Nodes/Helpers/Map/NodeMapSwiftUI.swift | 36 ++++- 2 files changed, 114 insertions(+), 68 deletions(-) diff --git a/Meshtastic/Views/Nodes/Helpers/Map/MapContent/NodeMapContent.swift b/Meshtastic/Views/Nodes/Helpers/Map/MapContent/NodeMapContent.swift index 6059a57c..02635dd8 100644 --- a/Meshtastic/Views/Nodes/Helpers/Map/MapContent/NodeMapContent.swift +++ b/Meshtastic/Views/Nodes/Helpers/Map/MapContent/NodeMapContent.swift @@ -11,42 +11,32 @@ import CoreData struct NodeMapContent: MapContent { @ObservedObject var node: NodeInfoEntity - @State var showUserLocation: Bool = false - @State var positions: [PositionEntity] = [] /// Map State User Defaults @AppStorage("meshMapShowNodeHistory") private var showNodeHistory = false @AppStorage("meshMapShowRouteLines") private var showRouteLines = false - @AppStorage("enableMapWaypoints") private var showWaypoints = true @AppStorage("enableMapConvexHull") private var showConvexHull = false - @AppStorage("enableMapTraffic") private var showTraffic: Bool = false - @AppStorage("enableMapPointsOfInterest") private var showPointsOfInterest: Bool = false - @AppStorage("mapLayer") private var selectedMapLayer: MapLayer = .hybrid // Map Configuration @Namespace var mapScope - @State var mapStyle: MapStyle = MapStyle.hybrid(elevation: .realistic, pointsOfInterest: .all, showsTraffic: true) - @State var position = MapCameraPosition.automatic - @State var scene: MKLookAroundScene? - @State var isLookingAround = false - @State var isShowingAltitude = false - @State var isEditingSettings = false @State var selectedPosition: PositionEntity? - @State var isMeshMap = false @MapContentBuilder var nodeMap: some MapContent { let positionArray = node.positions?.array as? [PositionEntity] ?? [] - let lineCoords = positionArray.compactMap({(position) -> CLLocationCoordinate2D in - return position.nodeCoordinate ?? LocationsHandler.DefaultLocation - }) /// Node Color from node.num let nodeColor = UIColor(hex: UInt32(node.num)) + let nodeColorSwift = Color(nodeColor) + let nodeBorderColor: Color = nodeColorSwift.isLight() ? .black : .white + + // Prerender node history point views as UIImages for speedup when there are thousands of history points + let prerenderedHistoryPointCircleImage = showNodeHistory ? prerenderHistoryPointCircle(fill: nodeColorSwift, stroke: nodeBorderColor) : UIImage() + let prerenderedHistoryPointArrowImage = showNodeHistory ? prerenderHistoryPointArrow(fill: nodeColorSwift, stroke: nodeBorderColor) : UIImage() + + let pf = PositionFlags(rawValue: Int(node.metadata?.positionFlags ?? 771)) /// Node Annotations - ForEach(node.positions?.array as? [PositionEntity] ?? [], id: \.id) { position in - - let pf = PositionFlags(rawValue: Int(position.nodePosition?.metadata?.positionFlags ?? 771)) + ForEach(positionArray, id: \.id) { position in let headingDegrees = Angle.degrees(Double(position.heading)) /// Reduced Precision Map Circle if position.latest && 12...15 ~= position.precisionBits { @@ -58,32 +48,6 @@ struct NodeMapContent: MapContent { .stroke(.white, lineWidth: 2) } } - let loraNodes = positions.filter { $0.nodePosition?.viaMqtt ?? true == false } - let loraCoords = Array(loraNodes).compactMap({(position) -> CLLocationCoordinate2D in - return position.nodeCoordinate ?? LocationsHandler.DefaultLocation - }) - /// Convex Hull - if showConvexHull { - if loraCoords.count > 0 { - let hull = loraCoords.getConvexHull() - MapPolygon(coordinates: hull) - .stroke(.blue, lineWidth: 3) - .foregroundStyle(.indigo.opacity(0.4)) - } - } - /// Route Lines - if showRouteLines { - let gradient = LinearGradient( - colors: [Color(nodeColor.lighter().lighter().lighter()), Color(nodeColor.lighter()), Color(nodeColor)], - startPoint: .leading, endPoint: .trailing - ) - let dashed = StrokeStyle( - lineWidth: 3, - lineCap: .round, lineJoin: .round, dash: [10, 10] - ) - MapPolyline(coordinates: lineCoords) - .stroke(gradient, style: dashed) - } /// Lastest Position Pin if position.latest { /// Node Annotations @@ -93,7 +57,7 @@ struct NodeMapContent: MapContent { if pf.contains(.Heading) { Image(systemName: pf.contains(.Speed) && position.speed > 1 ? "location.north" : "octagon") .padding(5) - .foregroundStyle(Color(nodeColor).isLight() ? .black : .white) + .foregroundStyle(nodeBorderColor) .background(Color(nodeColor.darker())) .clipShape(Circle()) .rotationEffect(headingDegrees) @@ -111,7 +75,7 @@ struct NodeMapContent: MapContent { Image(systemName: "flipphone") .symbolEffect(.pulse.byLayer) .padding(5) - .foregroundStyle(Color(nodeColor).isLight() ? .black : .white) + .foregroundStyle(nodeBorderColor) .background(Color(UIColor(hex: UInt32(node.num)).darker())) .clipShape(Circle()) .onTapGesture { @@ -133,27 +97,25 @@ struct NodeMapContent: MapContent { } /// Node History if showNodeHistory { - if position.latest == false && position.nodePosition?.favorite ?? false { - let pf = PositionFlags(rawValue: Int(position.nodePosition?.metadata?.positionFlags ?? 771)) + // Having showNodeHistory enabled can be quite slow if there are thousands of history points. + if position.latest == false && node.favorite { let headingDegrees = Angle.degrees(Double(position.heading)) Annotation("", coordinate: position.coordinate) { - LazyVStack { - if pf.contains(.Heading) { - Image(systemName: "location.north.circle") - .resizable() - .scaledToFit() - .foregroundStyle(Color(UIColor(hex: UInt32(position.nodePosition?.num ?? 0))).isLight() ? .black : .white) - .background(Color(UIColor(hex: UInt32(position.nodePosition?.num ?? 0)))) - .clipShape(Circle()) - .rotationEffect(headingDegrees) - .frame(width: 16, height: 16) - - } else { - Circle() - .fill(Color(UIColor(hex: UInt32(position.nodePosition?.num ?? 0)))) - .strokeBorder(Color(UIColor(hex: UInt32(position.nodePosition?.num ?? 0))).isLight() ? .black : .white, lineWidth: 2) - .frame(width: 12, height: 12) - } + if pf.contains(.Heading) { + Image(uiImage: prerenderedHistoryPointArrowImage) + .renderingMode(.original) + .interpolation(.none) + .rotationEffect(headingDegrees) + .frame(width: 16, height: 16) + .allowsHitTesting(false) + .accessibilityHidden(true) + } else { + Image(uiImage: prerenderedHistoryPointCircleImage) + .renderingMode(.original) + .interpolation(.none) + .frame(width: 12, height: 12) + .allowsHitTesting(false) + .accessibilityHidden(true) } } .annotationTitles(.hidden) @@ -161,6 +123,33 @@ struct NodeMapContent: MapContent { } } } + + // Shared coordinate list for Route Lines and Convex Hull + let allCoords: [CLLocationCoordinate2D] = (showRouteLines || showConvexHull) ? positionArray.compactMap(\.nodeCoordinate) : [] + + /// Route Lines + if showRouteLines { + let gradient = LinearGradient( + colors: [Color(nodeColor.lighter().lighter().lighter()), Color(nodeColor.lighter()), Color(nodeColor)], + startPoint: .leading, endPoint: .trailing + ) + let dashed = StrokeStyle( + lineWidth: 3, + lineCap: .round, lineJoin: .round, dash: [10, 10] + ) + MapPolyline(coordinates: allCoords) + .stroke(gradient, style: dashed) + } + + /// Convex Hull + if showConvexHull { + if allCoords.count > 0 { + let hull = allCoords.getConvexHull() + MapPolygon(coordinates: hull) + .stroke(.blue, lineWidth: 3) + .foregroundStyle(.indigo.opacity(0.4)) + } + } } @MapContentBuilder @@ -169,4 +158,29 @@ struct NodeMapContent: MapContent { nodeMap } } + + private func prerenderHistoryPointCircle(fill: Color, stroke: Color) -> UIImage { + // Render to UIImage once so we don't have to do a ton of vector operations and layers when there are thousands of history points. + let content = Circle() + .fill(fill) + .strokeBorder(stroke, lineWidth: 2) + .frame(width: 12, height: 12) + let renderer = ImageRenderer(content: content) + renderer.scale = UIScreen.main.scale + return renderer.uiImage! + } + + private func prerenderHistoryPointArrow(fill: Color, stroke: Color) -> UIImage { + // Render to UIImage once so we don't have to do a ton of vector operations and layers when there are thousands of history points. + let content = Image(systemName: "location.north.circle") + .resizable() + .scaledToFit() + .foregroundStyle(stroke) + .background(fill) + .clipShape(Circle()) + .frame(width: 16, height: 16) + let renderer = ImageRenderer(content: content) + renderer.scale = UIScreen.main.scale + return renderer.uiImage! + } } diff --git a/Meshtastic/Views/Nodes/Helpers/Map/NodeMapSwiftUI.swift b/Meshtastic/Views/Nodes/Helpers/Map/NodeMapSwiftUI.swift index 67707590..5181586c 100644 --- a/Meshtastic/Views/Nodes/Helpers/Map/NodeMapSwiftUI.swift +++ b/Meshtastic/Views/Nodes/Helpers/Map/NodeMapSwiftUI.swift @@ -9,6 +9,26 @@ import SwiftUI import CoreLocation import MapKit +struct NodeMapContentSignature: Equatable { + // Used to decide if NodeMapContent needs to be reevaluated. + // Only include fields that are used within NodeMapContent (or approximations like positionCount and lastPositionTime). + let nodeNum: Int64 + let positionCount: Int + let lastPositionTime: Date? + let showNodeHistory: Bool + let showRouteLines: Bool + let showConvexHull: Bool + let favorite: Bool +} + +private struct NodeMapContentEquatableWrapper: View, Equatable { + // Prevent slow, needless recomputation of NodeMapContent if the NodeMapContentSignature hasn't changed. + let signature: NodeMapContentSignature + @ViewBuilder let content: () -> Content + static func == (lhs: NodeMapContentEquatableWrapper, rhs: NodeMapContentEquatableWrapper) -> Bool { lhs.signature == rhs.signature } + var body: some View { content() } +} + struct NodeMapSwiftUI: View { @Environment(\.managedObjectContext) var context @EnvironmentObject var accessoryManager: AccessoryManager @@ -17,6 +37,9 @@ struct NodeMapSwiftUI: View { @State var showUserLocation: Bool = false @State var positions: [PositionEntity] = [] /// Map State User Defaults + @AppStorage("meshMapShowNodeHistory") private var showNodeHistory = false + @AppStorage("meshMapShowRouteLines") private var showRouteLines = false + @AppStorage("enableMapConvexHull") private var showConvexHull = false @AppStorage("enableMapTraffic") private var showTraffic: Bool = false @AppStorage("enableMapPointsOfInterest") private var showPointsOfInterest: Bool = false @AppStorage("mapLayer") private var selectedMapLayer: MapLayer = .hybrid @@ -91,9 +114,17 @@ struct NodeMapSwiftUI: View { } } + private var mapContentSignature: NodeMapContentSignature { + let positionCount = node.positions?.count ?? 0 + let lastPositionTime = (node.positions?.lastObject as? PositionEntity)?.time + return NodeMapContentSignature(nodeNum: node.num, positionCount: positionCount, lastPositionTime: lastPositionTime, showNodeHistory: showNodeHistory, showRouteLines: showRouteLines, showConvexHull: showConvexHull, favorite: node.favorite) + } + private var baseMap: some View { - Map(position: $position, bounds: MapCameraBounds(minimumDistance: 0, maximumDistance: .infinity), scope: mapScope) { - NodeMapContent(node: node) + NodeMapContentEquatableWrapper(signature: mapContentSignature) { + Map(position: $position, bounds: MapCameraBounds(minimumDistance: 0, maximumDistance: .infinity), scope: mapScope) { + NodeMapContent(node: node) + } } .mapScope(mapScope) .mapStyle(mapStyle) @@ -110,6 +141,7 @@ struct NodeMapSwiftUI: View { .mapControlVisibility(.visible) } .controlSize(.regular) + .transaction { $0.animation = nil } } private var lookAroundView: some View { From 2ee6cdfcba21a2c182edf8759d62f74077c56053 Mon Sep 17 00:00:00 2001 From: Mike Robbins Date: Thu, 30 Oct 2025 17:35:40 -0400 Subject: [PATCH 25/68] Fix wantRangeTestPackets to correctly follow rangeTestConfig.enabled (#1489) --- .../Accessory Manager/AccessoryManager+MQTT.swift | 10 ++++------ .../Accessory/Accessory Manager/AccessoryManager.swift | 2 +- .../Views/Settings/Config/Module/RangeTestConfig.swift | 2 ++ .../Settings/Config/Module/StoreForwardConfig.swift | 2 ++ 4 files changed, 9 insertions(+), 7 deletions(-) diff --git a/Meshtastic/Accessory/Accessory Manager/AccessoryManager+MQTT.swift b/Meshtastic/Accessory/Accessory Manager/AccessoryManager+MQTT.swift index 0a246f88..d8c5a35d 100644 --- a/Meshtastic/Accessory/Accessory Manager/AccessoryManager+MQTT.swift +++ b/Meshtastic/Accessory/Accessory Manager/AccessoryManager+MQTT.swift @@ -34,12 +34,10 @@ extension AccessoryManager { // Set initial unread message badge states appState.unreadChannelMessages = fetchedNodeInfo[0].myInfo?.unreadMessages(context: context) ?? 0 appState.unreadDirectMessages = fetchedNodeInfo[0].user?.unreadMessages(context: context, skipLastMessageCheck: true) ?? 0 // skipLastMessageCheck=true because we don't update lastMessage on our own connected node - } - if fetchedNodeInfo.count == 1 && fetchedNodeInfo[0].rangeTestConfig?.enabled == true { - wantRangeTestPackets = true - } - if fetchedNodeInfo.count == 1 && fetchedNodeInfo[0].storeForwardConfig?.enabled == true { - wantStoreAndForwardPackets = true + + // Set wantRangeTestPackets and wantStoreAndForwardPackets + wantRangeTestPackets = fetchedNodeInfo[0].rangeTestConfig?.enabled ?? false + wantStoreAndForwardPackets = fetchedNodeInfo[0].storeForwardConfig?.enabled ?? false } } catch { Logger.data.error("Failed to find a node info for the connected node \(error.localizedDescription, privacy: .public)") diff --git a/Meshtastic/Accessory/Accessory Manager/AccessoryManager.swift b/Meshtastic/Accessory/Accessory Manager/AccessoryManager.swift index f5a114ba..5e2e98c8 100644 --- a/Meshtastic/Accessory/Accessory Manager/AccessoryManager.swift +++ b/Meshtastic/Accessory/Accessory Manager/AccessoryManager.swift @@ -141,7 +141,7 @@ class AccessoryManager: ObservableObject, MqttClientProxyManagerDelegate { let transports: [any Transport] // Config - public var wantRangeTestPackets = true + public var wantRangeTestPackets = false var wantStoreAndForwardPackets = false var shouldAutomaticallyConnectToPreferredPeripheral = true diff --git a/Meshtastic/Views/Settings/Config/Module/RangeTestConfig.swift b/Meshtastic/Views/Settings/Config/Module/RangeTestConfig.swift index 83958ff7..a4bfbd85 100644 --- a/Meshtastic/Views/Settings/Config/Module/RangeTestConfig.swift +++ b/Meshtastic/Views/Settings/Config/Module/RangeTestConfig.swift @@ -110,6 +110,8 @@ struct RangeTestConfig: View { } .onChange(of: enabled) { _, newEnabled in if newEnabled != node?.rangeTestConfig?.enabled { hasChanges = true } + + // Note: even if this is the connected node, we don't have to update AccessoryManager.wantRangeTestPackets here, because the node will reboot after we save config changes, and we'll pick up the new value after we reconnect. } .onChange(of: save) { _, newSave in if newSave != node?.rangeTestConfig?.save { hasChanges = true } diff --git a/Meshtastic/Views/Settings/Config/Module/StoreForwardConfig.swift b/Meshtastic/Views/Settings/Config/Module/StoreForwardConfig.swift index 397945fb..bc35258d 100644 --- a/Meshtastic/Views/Settings/Config/Module/StoreForwardConfig.swift +++ b/Meshtastic/Views/Settings/Config/Module/StoreForwardConfig.swift @@ -167,6 +167,8 @@ struct StoreForwardConfig: View { } .onChange(of: enabled) { oldEnabled, newEnabled in if oldEnabled != newEnabled && newEnabled != node!.storeForwardConfig!.enabled { hasChanges = true } + + // Note: even if this is the connected node, we don't have to update AccessoryManager.wantStoreAndForwardPackets here, because the node will reboot after we save config changes, and we'll pick up the new value after we reconnect. } .onChange(of: isServer) { oldIsServer, newIsServer in if oldIsServer != newIsServer && newIsServer != node!.storeForwardConfig!.isRouter { hasChanges = true } From 0fcf4fdbcbbe636a5f0cae84dfce7e5199400851 Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Fri, 31 Oct 2025 09:08:46 -0700 Subject: [PATCH 26/68] Fix interval drop down formatter --- Localizable.xcstrings | 4 +-- .../Views/Settings/UpdateIntervalPicker.swift | 29 ++++++++++--------- 2 files changed, 18 insertions(+), 15 deletions(-) diff --git a/Localizable.xcstrings b/Localizable.xcstrings index c725645e..132dbb01 100644 --- a/Localizable.xcstrings +++ b/Localizable.xcstrings @@ -1486,8 +1486,8 @@ } } }, - "⚠️ The configured value: (%@ seconds) is not one of the optimized options." : { - "comment" : "A text warning that the configured update interval is not one of the optimized options.", + "⚠️ The configured value: (%@) is not one of the optimized options." : { + "comment" : "A warning label below the picker, indicating that the selected update interval is not one of the optimized options.", "isCommentAutoGenerated" : true }, "🦕 End of life Version 🦖 ☄️" : { diff --git a/Meshtastic/Views/Settings/UpdateIntervalPicker.swift b/Meshtastic/Views/Settings/UpdateIntervalPicker.swift index b7d6d153..c8601624 100644 --- a/Meshtastic/Views/Settings/UpdateIntervalPicker.swift +++ b/Meshtastic/Views/Settings/UpdateIntervalPicker.swift @@ -1,15 +1,15 @@ // -// UpdateIntervalPicker.swift -// Meshtastic +//  UpdateIntervalPicker.swift +//  Meshtastic // -// Copyright(c) Garth Vander Houwen 10/4/25. +//  Copyright(c) Garth Vander Houwen 10/4/25. // import SwiftUI struct UpdateIntervalPicker: View { let config: IntervalConfiguration let pickerLabel: String - let formatter = DateComponentsFormatter() + let formatter: DateComponentsFormatter // Make it a stored property @Binding var selectedInterval: UpdateInterval @@ -17,11 +17,14 @@ struct UpdateIntervalPicker: View { config.allowedCases .map { UpdateInterval(from: $0.rawValue) } } - + init(config: IntervalConfiguration, pickerLabel: String, selectedInterval: Binding) { self.config = config self.pickerLabel = pickerLabel self._selectedInterval = selectedInterval + let f = DateComponentsFormatter() + f.unitsStyle = .full + self.formatter = f } var body: some View { @@ -36,7 +39,7 @@ struct UpdateIntervalPicker: View { if isOutOfRange { let interval: TimeInterval = Double(selectedInterval.intValue) if let formattedString = formatter.string(from: interval) { - Text("⚠️ The configured value: (\(formattedString) seconds) is not one of the optimized options.") + Text("⚠️ The configured value: (\(formattedString)) is not one of the optimized options.") .font(.caption) .foregroundColor(.orange) } @@ -44,11 +47,11 @@ struct UpdateIntervalPicker: View { } } private var isOutOfRange: Bool { - switch selectedInterval.type { - case .manual: - return true - case .fixed(let fixedCase): - return !config.allowedCases.contains(fixedCase) - } - } + switch selectedInterval.type { + case .manual: + return true + case .fixed(let fixedCase): + return !config.allowedCases.contains(fixedCase) + } + } } From b4c749a978ce1599bcf9486e3b0926a299739e75 Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Sat, 1 Nov 2025 08:29:47 -0700 Subject: [PATCH 27/68] Clean up channel qr code functionality. --- Localizable.xcstrings | 5 + .../AccessoryManager+ToRadio.swift | 50 +++++----- Meshtastic/Extensions/Url.swift | 12 +++ Meshtastic/MeshtasticApp.swift | 91 ++++++++++--------- .../Views/Settings/SaveChannelQRCode.swift | 6 +- 5 files changed, 94 insertions(+), 70 deletions(-) diff --git a/Localizable.xcstrings b/Localizable.xcstrings index 132dbb01..0c8fab99 100644 --- a/Localizable.xcstrings +++ b/Localizable.xcstrings @@ -36770,7 +36770,12 @@ } } }, + "These settings will %@" : { + "comment" : "A paragraph below the title that explains what the user is about to do.", + "isCommentAutoGenerated" : true + }, "These settings will %@ channels. The current LoRa Config will be replaced, if there are substantial changes to the LoRa config the device will reboot" : { + "extractionState" : "stale", "localizations" : { "it" : { "stringUnit" : { diff --git a/Meshtastic/Accessory/Accessory Manager/AccessoryManager+ToRadio.swift b/Meshtastic/Accessory/Accessory Manager/AccessoryManager+ToRadio.swift index e0478902..57c2f701 100644 --- a/Meshtastic/Accessory/Accessory Manager/AccessoryManager+ToRadio.swift +++ b/Meshtastic/Accessory/Accessory Manager/AccessoryManager+ToRadio.swift @@ -509,32 +509,32 @@ extension AccessoryManager { let logString = String.localizedStringWithFormat("Sent a Channel for: %@ Channel Index %d".localized, String(deviceNum), chan.index) try await send(toRadio, debugDescription: logString) } - - // Save the LoRa Config and the device will reboot - var adminPacket = AdminMessage() - adminPacket.setConfig.lora = channelSet.loraConfig - adminPacket.setConfig.lora.configOkToMqtt = okToMQTT // Preserve users okToMQTT choice - var meshPacket: MeshPacket = MeshPacket() - meshPacket.to = UInt32(deviceNum) - meshPacket.from = UInt32(deviceNum) - meshPacket.id = UInt32.random(in: UInt32(UInt8.max).. Date: Sat, 1 Nov 2025 16:45:02 -0400 Subject: [PATCH 28/68] perferredPeripheralId fix --- .../AccessoryManager+Connect.swift | 13 ++++--------- 1 file changed, 4 insertions(+), 9 deletions(-) diff --git a/Meshtastic/Accessory/Accessory Manager/AccessoryManager+Connect.swift b/Meshtastic/Accessory/Accessory Manager/AccessoryManager+Connect.swift index d63f8acf..3e2dd76f 100644 --- a/Meshtastic/Accessory/Accessory Manager/AccessoryManager+Connect.swift +++ b/Meshtastic/Accessory/Accessory Manager/AccessoryManager+Connect.swift @@ -66,15 +66,6 @@ extension AccessoryManager { Logger.transport.info("[Accessory] Event stream closed") } self.activeConnection = (device: device, connection: connection) - - // if we don't have a peripheralId, set it now at the beginning of the - // connect process (because I think it is used in other parts of the app - // during the connect process? - // Otherwise, UserDefault.preferredPeripheralId is set in the Connect - // view, as part of the "Connect to new radio?" confirmation dialog logic. - if UserDefaults.preferredPeripheralId.isEmpty { - UserDefaults.preferredPeripheralId = device.id.uuidString - } } catch let error as CBError where error.code == .peerRemovedPairingInformation { await self.connectionStepper?.cancelCurrentlyExecutingStep(withError: AccessoryError.coreBluetoothError(error), cancelFullProcess: true) } @@ -119,6 +110,10 @@ extension AccessoryManager { Logger.transport.info("🔗👟 [Connect] Step 5: Send wantConfig (database)") self.updateState(.retrievingDatabase(nodeCount: 0)) self.allowDisconnect = true + + Logger.transport.info("🔗 Saving preferredPeripheralId: \(device.id.uuidString)") + UserDefaults.preferredPeripheralId = device.id.uuidString + try await self.sendWantDatabase() } From feb9cf1aa90f9d6a5cba702a9066aac7261a6cd3 Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Sun, 2 Nov 2025 08:49:33 -0800 Subject: [PATCH 29/68] Set opt in --- Meshtastic/Views/Settings/Config/Module/MQTTConfig.swift | 1 + 1 file changed, 1 insertion(+) diff --git a/Meshtastic/Views/Settings/Config/Module/MQTTConfig.swift b/Meshtastic/Views/Settings/Config/Module/MQTTConfig.swift index 985bd878..5336c5ef 100644 --- a/Meshtastic/Views/Settings/Config/Module/MQTTConfig.swift +++ b/Meshtastic/Views/Settings/Config/Module/MQTTConfig.swift @@ -263,6 +263,7 @@ struct MQTTConfig: View { mqtt.jsonEnabled = self.jsonEnabled mqtt.tlsEnabled = self.tlsEnabled mqtt.mapReportingEnabled = self.mapReportingEnabled + mqtt.mapReportSettings.shouldReportLocation = UserDefaults.mapReportingOptIn mqtt.mapReportSettings.positionPrecision = UInt32(self.mapPositionPrecision) mqtt.mapReportSettings.publishIntervalSecs = UInt32(self.mapPublishIntervalSecs.intValue) Task { From 872c1ef7eedd06c37c086b23c5d305e16fba1420 Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Sun, 2 Nov 2025 08:54:37 -0800 Subject: [PATCH 30/68] Retry once 5 second timer. dont throw the error --- .../Accessory Manager/AccessoryManager+Connect.swift | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/Meshtastic/Accessory/Accessory Manager/AccessoryManager+Connect.swift b/Meshtastic/Accessory/Accessory Manager/AccessoryManager+Connect.swift index 3e2dd76f..84f0ba49 100644 --- a/Meshtastic/Accessory/Accessory Manager/AccessoryManager+Connect.swift +++ b/Meshtastic/Accessory/Accessory Manager/AccessoryManager+Connect.swift @@ -267,7 +267,7 @@ actor SequentialSteps { var isRunning: Bool = false var externalError: Error? - init(maxRetries: Int = 1, retryDelay: Duration = .seconds(3), @StepsBuilder _ builder: () -> [Step]) { + init(maxRetries: Int = 1, retryDelay: Duration = .seconds(5), @StepsBuilder _ builder: () -> [Step]) { self.maxRetries = maxRetries self.retryDelay = retryDelay self.steps = builder() @@ -352,7 +352,8 @@ actor SequentialSteps { return } isRunning = false - throw AccessoryError.tooManyRetries + return + //throw AccessoryError.tooManyRetries } func cancel() { From 0f90d8497a5b7fae2f522c923d3bec9ebdb99f3a Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Wed, 5 Nov 2025 16:21:10 -0800 Subject: [PATCH 31/68] Queue for peripherals --- .../Bluetooth Low Energy/BLETransport.swift | 50 ++++++++++++------- 1 file changed, 31 insertions(+), 19 deletions(-) diff --git a/Meshtastic/Accessory/Transports/Bluetooth Low Energy/BLETransport.swift b/Meshtastic/Accessory/Transports/Bluetooth Low Energy/BLETransport.swift index aa1a32d4..12c9e33c 100644 --- a/Meshtastic/Accessory/Transports/Bluetooth Low Energy/BLETransport.swift +++ b/Meshtastic/Accessory/Transports/Bluetooth Low Energy/BLETransport.swift @@ -164,27 +164,39 @@ class BLETransport: Transport { } } + private let peripheralsQueue = DispatchQueue(label: "com.meshtastic.peripheralAccess", qos: .default) + func didDiscover(peripheral: CBPeripheral, rssi: NSNumber) { + guard !restoreInProgress else { return } - - let id = peripheral.identifier - let isNew = discoveredPeripherals[id] == nil - if isNew { - discoveredPeripherals[id] = (peripheral, Date()) - } - let device = Device(id: id, - name: peripheral.name ?? "Unknown", - transportType: .ble, - identifier: id.uuidString, - rssi: rssi.intValue) - if isNew { - Logger.transport.debug("🛜 [BLE] Did Discover new device: \(peripheral.name ?? "Unknown", privacy: .public) (\(peripheral.identifier, privacy: .public))") - discoveredDeviceContinuation?.yield(.deviceFound(device)) - } else { - let rssiVal = rssi.intValue - let deviceId = id - discoveredPeripherals[id]?.lastSeen = Date() - discoveredDeviceContinuation?.yield(.deviceReportedRssi(deviceId, rssiVal)) + + // Use the queue to ensure thread-safe access to the dictionary + peripheralsQueue.async { + let id = peripheral.identifier + let isNew = self.discoveredPeripherals[id] == nil + + // Update the dictionary + if isNew { + self.discoveredPeripherals[id] = (peripheral, Date()) + } else { + self.discoveredPeripherals[id]?.lastSeen = Date() + } + + let device = Device(id: id, + name: peripheral.name ?? "Unknown", + transportType: .ble, + identifier: id.uuidString, + rssi: rssi.intValue) + + // Safely yield results + if isNew { + Logger.transport.debug("🛜 [BLE] Did Discover new device: \(peripheral.name ?? "Unknown", privacy: .public) (\(peripheral.identifier, privacy: .public))") + self.discoveredDeviceContinuation?.yield(.deviceFound(device)) + } else { + let rssiVal = rssi.intValue + let deviceId = id + self.discoveredDeviceContinuation?.yield(.deviceReportedRssi(deviceId, rssiVal)) + } } } From ec5dfd5ae3adc48ed411544847c2374e645bb958 Mon Sep 17 00:00:00 2001 From: Benjamin Faershtein <119711889+RCGV1@users.noreply.github.com> Date: Thu, 6 Nov 2025 10:10:25 -0800 Subject: [PATCH 32/68] Fix: hoplimit of dms would always fallback to hops away of the node even when configured hops was higher (#1495) * fix hops setting in dms * Fix hops for exchange position * Final fix --- .../Accessory/Accessory Manager/AccessoryManager+ToRadio.swift | 2 +- .../Views/Nodes/Helpers/Actions/ExchangePositionsButton.swift | 3 ++- Meshtastic/Views/Nodes/Helpers/NodeDetail.swift | 3 ++- 3 files changed, 5 insertions(+), 3 deletions(-) diff --git a/Meshtastic/Accessory/Accessory Manager/AccessoryManager+ToRadio.swift b/Meshtastic/Accessory/Accessory Manager/AccessoryManager+ToRadio.swift index 57c2f701..afdd04f5 100644 --- a/Meshtastic/Accessory/Accessory Manager/AccessoryManager+ToRadio.swift +++ b/Meshtastic/Accessory/Accessory Manager/AccessoryManager+ToRadio.swift @@ -350,7 +350,7 @@ extension AccessoryManager { if toUserNum > 0 { meshPacket.to = UInt32(toUserNum) let hopsAway = newMessage.toUser?.userNode?.hopsAway ?? 0 - if hopsAway > Int32(truncatingIfNeeded: newMessage.toUser?.userNode?.loRaConfig?.hopLimit ?? 0) { + if hopsAway > Int32(truncatingIfNeeded: newMessage.fromUser?.userNode?.loRaConfig?.hopLimit ?? 0) { meshPacket.hopLimit = UInt32(truncatingIfNeeded: hopsAway) } } else { diff --git a/Meshtastic/Views/Nodes/Helpers/Actions/ExchangePositionsButton.swift b/Meshtastic/Views/Nodes/Helpers/Actions/ExchangePositionsButton.swift index 05747fa9..f303f21e 100644 --- a/Meshtastic/Views/Nodes/Helpers/Actions/ExchangePositionsButton.swift +++ b/Meshtastic/Views/Nodes/Helpers/Actions/ExchangePositionsButton.swift @@ -3,6 +3,7 @@ import SwiftUI struct ExchangePositionsButton: View { var node: NodeInfoEntity + var connectedNode: NodeInfoEntity @EnvironmentObject var accessoryManager: AccessoryManager @@ -10,7 +11,7 @@ struct ExchangePositionsButton: View { @State private var isPresentingPositionFailedAlert: Bool = false var body: some View { - let hopsAway = Int32(truncatingIfNeeded: node.hopsAway > node.loRaConfig?.hopLimit ?? 0 ? node.hopsAway : node.loRaConfig?.hopLimit ?? 0) + let hopsAway = Int32(truncatingIfNeeded: node.hopsAway > connectedNode.loRaConfig?.hopLimit ?? 0 ? node.hopsAway : connectedNode.loRaConfig?.hopLimit ?? 0) Button { Task { do { diff --git a/Meshtastic/Views/Nodes/Helpers/NodeDetail.swift b/Meshtastic/Views/Nodes/Helpers/NodeDetail.swift index 7ad12716..efdcabb3 100644 --- a/Meshtastic/Views/Nodes/Helpers/NodeDetail.swift +++ b/Meshtastic/Views/Nodes/Helpers/NodeDetail.swift @@ -460,7 +460,8 @@ struct NodeDetail: View { } } ExchangePositionsButton( - node: node + node: node, + connectedNode: connectedNode ) TraceRouteButton( node: node From b51b5aaec039ecc3e497f845cef257989c8196e4 Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Tue, 18 Nov 2025 06:42:19 -0800 Subject: [PATCH 33/68] Don't favorite client base --- .../Accessory/Accessory Manager/AccessoryManager+ToRadio.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Meshtastic/Accessory/Accessory Manager/AccessoryManager+ToRadio.swift b/Meshtastic/Accessory/Accessory Manager/AccessoryManager+ToRadio.swift index afdd04f5..ca219430 100644 --- a/Meshtastic/Accessory/Accessory Manager/AccessoryManager+ToRadio.swift +++ b/Meshtastic/Accessory/Accessory Manager/AccessoryManager+ToRadio.swift @@ -333,7 +333,7 @@ extension AccessoryManager { var contact = SharedContact() contact.manuallyVerified = false contact.nodeNum = UInt32(truncatingIfNeeded: user.num) - user.userNode?.favorite = true + user.userNode?.favorite = user.userNode?.deviceConfig?.role ?? 0 != DeviceRoles.clientBase.rawValue contact.user = user.toProto() do { let contactString = try contact.serializedData().base64EncodedString() From 6aca186ed91db6cd568e867ec4bf11578d9fd0e1 Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Tue, 18 Nov 2025 06:50:28 -0800 Subject: [PATCH 34/68] Update device hardware --- Meshtastic/Resources/DeviceHardware.json | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/Meshtastic/Resources/DeviceHardware.json b/Meshtastic/Resources/DeviceHardware.json index 39b61693..fcaf48cd 100644 --- a/Meshtastic/Resources/DeviceHardware.json +++ b/Meshtastic/Resources/DeviceHardware.json @@ -1181,5 +1181,18 @@ "images": [ "rak_3312.svg" ] + }, + { + "hwModel": 115, + "hwModelSlug": "THINKNODE_M3", + "platformioTarget": "thinknode_m3", + "architecture": "nrf52840", + "activelySupported": false, + "supportLevel": 1, + "displayName": "ThinkNode M3", + "tags": [ + "Elecrow" + ], + "requiresDfu": true } ] From 5762677946a244ef1496dff2846fe70e7d85ea69 Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Tue, 18 Nov 2025 11:28:18 -0800 Subject: [PATCH 35/68] Prevent nil environment metrics --- .../Views/Nodes/Helpers/NodeDetail.swift | 40 +++++++++---------- 1 file changed, 20 insertions(+), 20 deletions(-) diff --git a/Meshtastic/Views/Nodes/Helpers/NodeDetail.swift b/Meshtastic/Views/Nodes/Helpers/NodeDetail.swift index efdcabb3..69e6ee4e 100644 --- a/Meshtastic/Views/Nodes/Helpers/NodeDetail.swift +++ b/Meshtastic/Views/Nodes/Helpers/NodeDetail.swift @@ -40,7 +40,7 @@ struct NodeDetail: View { context: context ) Section("Hardware") { - + NodeInfoItem(node: node) // .id("topOfList") } @@ -260,18 +260,18 @@ struct NodeDetail: View { // Update the state with the new height self.environmentSectionHeight = newHeight } - } else { + } else if let metrics = node.latestEnvironmentMetrics { // 👈 REFACTORED: Unwraps metrics safely VStack { - if node.latestEnvironmentMetrics?.iaq ?? -1 > 0 { - IndoorAirQuality(iaq: Int(node.latestEnvironmentMetrics?.iaq ?? 0), displayMode: .gradient) + if metrics.iaq ?? -1 > 0 { // Use unwrapped 'metrics' + IndoorAirQuality(iaq: Int(metrics.iaq ?? 0), displayMode: .gradient) .padding(.vertical) } LazyVGrid(columns: gridItemLayout) { - if let temperature = node.latestEnvironmentMetrics?.temperature?.shortFormattedTemperature() { + if let temperature = metrics.temperature?.shortFormattedTemperature() { WeatherConditionsCompactWidget(temperature: String(temperature), symbolName: "cloud.sun", description: "TEMP") } - if let humidity = node.latestEnvironmentMetrics?.relativeHumidity { - if let temperature = node.latestEnvironmentMetrics?.temperature { + if let humidity = metrics.relativeHumidity { + if let temperature = metrics.temperature { let dewPoint = calculateDewPoint(temp: temperature, relativeHumidity: humidity) .formatted(.number.precision(.fractionLength(0))) + "°" HumidityCompactWidget(humidity: Int(humidity), dewPoint: dewPoint) @@ -279,17 +279,17 @@ struct NodeDetail: View { HumidityCompactWidget(humidity: Int(humidity), dewPoint: nil) } } - if let pressure = node.latestEnvironmentMetrics?.barometricPressure { + if let pressure = metrics.barometricPressure { PressureCompactWidget(pressure: pressure.formatted(.number.precision(.fractionLength(2))), unit: "hPA", low: pressure <= 1009.144) } - if let windSpeed = node.latestEnvironmentMetrics?.windSpeed { + if let windSpeed = metrics.windSpeed { let windSpeedMeasurement = Measurement(value: Double(windSpeed), unit: UnitSpeed.metersPerSecond) - let windGust = node.latestEnvironmentMetrics?.windGust.map { Measurement(value: Double($0), unit: UnitSpeed.metersPerSecond) } - let direction = cardinalValue(from: Double(node.latestEnvironmentMetrics?.windDirection ?? 0)) + let windGust = metrics.windGust.map { Measurement(value: Double($0), unit: UnitSpeed.metersPerSecond) } + let direction = cardinalValue(from: Double(metrics.windDirection ?? 0)) // Use unwrapped 'metrics' WindCompactWidget(speed: windSpeedMeasurement.formatted(.measurement(width: .abbreviated, numberFormatStyle: .number.precision(.fractionLength(0)))), - gust: node.latestEnvironmentMetrics?.windGust ?? 0.0 > 0.0 ? windGust?.formatted(.measurement(width: .abbreviated, numberFormatStyle: .number.precision(.fractionLength(0)))) : "", direction: direction) + gust: metrics.windGust ?? 0.0 > 0.0 ? windGust?.formatted(.measurement(width: .abbreviated, numberFormatStyle: .number.precision(.fractionLength(0)))) : "", direction: direction) } - if let rainfall1h = node.latestEnvironmentMetrics?.rainfall1H { + if let rainfall1h = metrics.rainfall1H { let locale = NSLocale.current as NSLocale let usesMetricSystem = locale.usesMetricSystem // Returns true for metric (mm), false for imperial (inches) let unit = usesMetricSystem ? UnitLength.millimeters : UnitLength.inches @@ -299,7 +299,7 @@ struct NodeDetail: View { let formattedRain = measurement.converted(to: unit).value.formatted(.number.precision(.fractionLength(decimals))) RainfallCompactWidget(timespan: .rainfall1H, rainfall: formattedRain, unit: unitLabel) } - if let rainfall24h = node.latestEnvironmentMetrics?.rainfall24H { + if let rainfall24h = metrics.rainfall24H { let locale = NSLocale.current as NSLocale let usesMetricSystem = locale.usesMetricSystem // Returns true for metric (mm), false for imperial (inches) let unit = usesMetricSystem ? UnitLength.millimeters : UnitLength.inches @@ -309,26 +309,26 @@ struct NodeDetail: View { let formattedRain = measurement.converted(to: unit).value.formatted(.number.precision(.fractionLength(decimals))) RainfallCompactWidget(timespan: .rainfall24H, rainfall: formattedRain, unit: unitLabel) } - if let radiation = node.latestEnvironmentMetrics?.radiation { + if let radiation = metrics.radiation { RadiationCompactWidget(radiation: radiation.formatted(.number.precision(.fractionLength(1))), unit: "µR/hr") } - if let weight = node.latestEnvironmentMetrics?.weight { + if let weight = metrics.weight { WeightCompactWidget(weight: weight.formatted(.number.precision(.fractionLength(1))), unit: "kg") } - if let distance = node.latestEnvironmentMetrics?.distance { + if let distance = metrics.distance { DistanceCompactWidget(distance: distance.formatted(.number.precision(.fractionLength(0))), unit: "mm") } - if let soilTemperature = node.latestEnvironmentMetrics?.soilTemperature { + if let soilTemperature = metrics.soilTemperature { let locale = NSLocale.current as NSLocale let localeUnit = locale.object(forKey: NSLocale.Key(rawValue: "kCFLocaleTemperatureUnitKey")) let unit = localeUnit as? String ?? "Celsius" == "Fahrenheit" ? "°F" : "°C" SoilTemperatureCompactWidget(temperature: soilTemperature.localeTemperature().formatted(.number.precision(.fractionLength(0))), unit: unit) } - if let soilMoisture = node.latestEnvironmentMetrics?.soilMoisture { + if let soilMoisture = metrics.soilMoisture { SoilMoistureCompactWidget(moisture: soilMoisture.formatted(.number.precision(.fractionLength(0))), unit: "%") } } - .padding(node.latestEnvironmentMetrics?.iaq ?? -1 > 0 ? .bottom : .vertical) + .padding(metrics.iaq ?? -1 > 0 ? .bottom : .vertical) } } } From 570789610cf1f90b8e2a3993df4e15f33f8114e1 Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Mon, 24 Nov 2025 12:27:08 -0800 Subject: [PATCH 36/68] Bump datadog sdk --- Meshtastic.xcodeproj/project.pbxproj | 2 +- Meshtastic.xcworkspace/xcshareddata/swiftpm/Package.resolved | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Meshtastic.xcodeproj/project.pbxproj b/Meshtastic.xcodeproj/project.pbxproj index ee8aba2a..83759679 100644 --- a/Meshtastic.xcodeproj/project.pbxproj +++ b/Meshtastic.xcodeproj/project.pbxproj @@ -2265,7 +2265,7 @@ requirement = { kind = versionRange; maximumVersion = 4.0.0; - minimumVersion = 3.1.0; + minimumVersion = 3.3.0; }; }; 259792242C2F10B600AD1659 /* XCRemoteSwiftPackageReference "swift-protobuf" */ = { diff --git a/Meshtastic.xcworkspace/xcshareddata/swiftpm/Package.resolved b/Meshtastic.xcworkspace/xcshareddata/swiftpm/Package.resolved index 84f5fd5e..53375016 100644 --- a/Meshtastic.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/Meshtastic.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -15,8 +15,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/DataDog/dd-sdk-ios.git", "state" : { - "revision" : "a193cab2786b704ebc98c290a7cc8a3165f636bb", - "version" : "3.1.0" + "revision" : "8d67e973ff4a958cb536263cb816646ee904c508", + "version" : "3.3.0" } }, { From a91c62bfe2068d220e4ea61e4368aa28e8a42256 Mon Sep 17 00:00:00 2001 From: Benjamin Faershtein <119711889+RCGV1@users.noreply.github.com> Date: Tue, 9 Dec 2025 22:45:02 -0800 Subject: [PATCH 37/68] fix setting device telemetry enabled (#1515) * Update Muzi R1 Neo to actively supported * fix setting device telemetry enabled --------- Co-authored-by: Jonathan Bennett Co-authored-by: Ben Meadors --- Meshtastic/Persistence/UpdateCoreData.swift | 2 ++ Meshtastic/Resources/DeviceHardware.json | 1 + 2 files changed, 3 insertions(+) diff --git a/Meshtastic/Persistence/UpdateCoreData.swift b/Meshtastic/Persistence/UpdateCoreData.swift index ad9116b9..380193a8 100644 --- a/Meshtastic/Persistence/UpdateCoreData.swift +++ b/Meshtastic/Persistence/UpdateCoreData.swift @@ -1503,6 +1503,7 @@ func upsertTelemetryModuleConfigPacket(config: ModuleConfig.TelemetryConfig, nod if fetchedNode[0].telemetryConfig == nil { let newTelemetryConfig = TelemetryConfigEntity(context: context) newTelemetryConfig.deviceUpdateInterval = Int32(truncatingIfNeeded: config.deviceUpdateInterval) + newTelemetryConfig.deviceTelemetryEnabled = config.deviceTelemetryEnabled newTelemetryConfig.environmentUpdateInterval = Int32(truncatingIfNeeded: config.environmentUpdateInterval) newTelemetryConfig.environmentMeasurementEnabled = config.environmentMeasurementEnabled newTelemetryConfig.environmentScreenEnabled = config.environmentScreenEnabled @@ -1513,6 +1514,7 @@ func upsertTelemetryModuleConfigPacket(config: ModuleConfig.TelemetryConfig, nod fetchedNode[0].telemetryConfig = newTelemetryConfig } else { fetchedNode[0].telemetryConfig?.deviceUpdateInterval = Int32(truncatingIfNeeded: config.deviceUpdateInterval) + fetchedNode[0].telemetryConfig?.deviceTelemetryEnabled = config.deviceTelemetryEnabled fetchedNode[0].telemetryConfig?.environmentUpdateInterval = Int32(truncatingIfNeeded: config.environmentUpdateInterval) fetchedNode[0].telemetryConfig?.environmentMeasurementEnabled = config.environmentMeasurementEnabled fetchedNode[0].telemetryConfig?.environmentScreenEnabled = config.environmentScreenEnabled diff --git a/Meshtastic/Resources/DeviceHardware.json b/Meshtastic/Resources/DeviceHardware.json index fcaf48cd..a49e973e 100644 --- a/Meshtastic/Resources/DeviceHardware.json +++ b/Meshtastic/Resources/DeviceHardware.json @@ -1196,3 +1196,4 @@ "requiresDfu": true } ] + From 13fd9c3a938a21c62779f86f636bcea5a9e7eac5 Mon Sep 17 00:00:00 2001 From: Benjamin Faershtein <119711889+RCGV1@users.noreply.github.com> Date: Tue, 9 Dec 2025 22:46:14 -0800 Subject: [PATCH 38/68] Don't subscribe to mqtt topic if downlink is not on (#1501) * Dont sub if no downlink * moved reload mqtt connect config --- .../AccessoryManager+MQTT.swift | 9 +++++++-- .../Helpers/Mqtt/MqttClientProxyManager.swift | 16 ++++++++++++++++ Meshtastic/Views/Settings/Channels.swift | 2 +- 3 files changed, 24 insertions(+), 3 deletions(-) diff --git a/Meshtastic/Accessory/Accessory Manager/AccessoryManager+MQTT.swift b/Meshtastic/Accessory/Accessory Manager/AccessoryManager+MQTT.swift index d8c5a35d..4da8c739 100644 --- a/Meshtastic/Accessory/Accessory Manager/AccessoryManager+MQTT.swift +++ b/Meshtastic/Accessory/Accessory Manager/AccessoryManager+MQTT.swift @@ -49,8 +49,12 @@ extension AccessoryManager { func onMqttConnected() { mqttProxyConnected = true mqttError = "" - Logger.services.info("📲 [MQTT Client Proxy] onMqttConnected now subscribing to \(self.mqttManager.topic, privacy: .public).") - mqttManager.mqttClientProxy?.subscribe(mqttManager.topic) + if mqttManager.shouldSubscribe { + Logger.services.info("📲 [MQTT Client Proxy] onMqttConnected now subscribing to \(self.mqttManager.topic, privacy: .public).") + mqttManager.mqttClientProxy?.subscribe(mqttManager.topic) + } else { + Logger.services.info("📲 [MQTT Client Proxy] onMqttConnected not subscribing since downlink is not on") + } } func onMqttDisconnected() { @@ -81,3 +85,4 @@ extension AccessoryManager { Logger.services.info("📲 [MQTT Client Proxy] onMqttError: \(message, privacy: .public)") } } + diff --git a/Meshtastic/Helpers/Mqtt/MqttClientProxyManager.swift b/Meshtastic/Helpers/Mqtt/MqttClientProxyManager.swift index 853533e9..936f23ad 100644 --- a/Meshtastic/Helpers/Mqtt/MqttClientProxyManager.swift +++ b/Meshtastic/Helpers/Mqtt/MqttClientProxyManager.swift @@ -25,6 +25,7 @@ class MqttClientProxyManager { var mqttClientProxy: CocoaMQTT? var topic = "msh" var debugLog = false + var shouldSubscribe = true func connectFromConfigSettings(node: NodeInfoEntity) { let originalAddress = node.mqttConfig?.address ?? "mqtt.meshtastic.org" let defaultServerAddress = "mqtt.meshtastic.org" @@ -43,6 +44,20 @@ class MqttClientProxyManager { let port = defaultServerPort let root = node.mqttConfig?.root?.count ?? 0 > 0 ? node.mqttConfig?.root : "msh" let prefix = root! + // Safely iterate channels and determine if any has downlink enabled + var hasAnyDownlinkEnabled = false + if let anyChannels = node.myInfo?.channels as? NSOrderedSet { + let channelEntities: [ChannelEntity] = anyChannels.array.compactMap { $0 as? ChannelEntity } + for channel in channelEntities { + if channel.downlinkEnabled == true { + hasAnyDownlinkEnabled = true + break + } + } + } + + shouldSubscribe = hasAnyDownlinkEnabled + topic = prefix + "/2/e" + "/#" // Require opt in to map report terms to connect if node.mqttConfig?.mapReportingEnabled ?? false && UserDefaults.mapReportingOptIn || !(node.mqttConfig?.mapReportingEnabled ?? false) { @@ -169,3 +184,4 @@ extension MqttClientProxyManager: CocoaMQTTDelegate { Logger.mqtt.debug("📲 [MQTT Client Proxy] pong") } } + diff --git a/Meshtastic/Views/Settings/Channels.swift b/Meshtastic/Views/Settings/Channels.swift index 29f76966..f5c89071 100644 --- a/Meshtastic/Views/Settings/Channels.swift +++ b/Meshtastic/Views/Settings/Channels.swift @@ -217,13 +217,13 @@ struct Channels: View { } Task { _ = try await accessoryManager.saveChannel(channel: channel, fromUser: node!.user!, toUser: node!.user!) - Task { @MainActor in selectedChannel = nil channelName = "" channelRole = 2 hasChanges = false } + accessoryManager.mqttManager.connectFromConfigSettings(node: node!) } } label: { Label("Save", systemImage: "square.and.arrow.down") From b57ba1557c6e9b5caf10ea028fd3f327da8bac4d Mon Sep 17 00:00:00 2001 From: Charles Pinesky <25388414+Vaidios@users.noreply.github.com> Date: Wed, 10 Dec 2025 07:51:33 +0100 Subject: [PATCH 39/68] Preview enabled in connected devices (#1509) * Update Muzi R1 Neo to actively supported * Preview enabled in connected devices * Fixing indentation * Fixing indentation --------- Co-authored-by: Jonathan Bennett Co-authored-by: Ben Meadors --- .../Views/Helpers/ConnectedDevice.swift | 86 ++++++++++++------- .../Views/Helpers/RXTXIndicatorView.swift | 8 +- 2 files changed, 58 insertions(+), 36 deletions(-) diff --git a/Meshtastic/Views/Helpers/ConnectedDevice.swift b/Meshtastic/Views/Helpers/ConnectedDevice.swift index 1cb46948..deb26509 100644 --- a/Meshtastic/Views/Helpers/ConnectedDevice.swift +++ b/Meshtastic/Views/Helpers/ConnectedDevice.swift @@ -1,22 +1,31 @@ /* -Abstract: -A view draws the indicator used in the upper right corner for views using BLE -*/ + Abstract: + A view draws the indicator used in the upper right corner for views using BLE + */ import SwiftUI struct ConnectedDevice: View { - @EnvironmentObject var accessoryManager: AccessoryManager - var deviceConnected: Bool - var name: String - var mqttProxyConnected: Bool = false - var mqttUplinkEnabled: Bool = false - var mqttDownlinkEnabled: Bool = false - var mqttTopic: String = "" - var phoneOnly: Bool = false - var showActivityLights: Bool - - init(deviceConnected: Bool, name: String, mqttProxyConnected: Bool = false, mqttUplinkEnabled: Bool = false, mqttDownlinkEnabled: Bool = false, mqttTopic: String = "", phoneOnly: Bool = false, showActivityLights: Bool = true) { + + let deviceConnected: Bool + let name: String + let mqttProxyConnected: Bool + let mqttUplinkEnabled: Bool + let mqttDownlinkEnabled: Bool + let mqttTopic: String + let phoneOnly: Bool + let showActivityLights: Bool + + init( + deviceConnected: Bool, + name: String, + mqttProxyConnected: Bool = false, + mqttUplinkEnabled: Bool = false, + mqttDownlinkEnabled: Bool = false, + mqttTopic: String = "", + phoneOnly: Bool = false, + showActivityLights: Bool = true + ) { self.deviceConnected = deviceConnected self.name = name self.mqttProxyConnected = mqttProxyConnected @@ -27,12 +36,12 @@ struct ConnectedDevice: View { self.showActivityLights = showActivityLights } - var body: some View { + var body: some View { HStack { if showActivityLights { RXTXIndicatorWidget() } - if (phoneOnly && UIDevice.current.userInterfaceIdiom == .phone) || !phoneOnly { + if (phoneOnly && UIDevice.current.userInterfaceIdiom == .phone) || !phoneOnly { if deviceConnected { // Create an HStack for connected state with proper accessibility HStack { @@ -64,24 +73,37 @@ struct ConnectedDevice: View { .accessibilityElement(children: .ignore) .accessibilityLabel("No Bluetooth device connected".localized) } - } + } } .if(.os26) { $0.padding(.leading, 5.0) } - } + } } -struct ConnectedDevice_Previews: PreviewProvider { - static var previews: some View { - VStack(alignment: .trailing) { - ConnectedDevice(deviceConnected: true, name: "MEMO", mqttProxyConnected: true) - ConnectedDevice(deviceConnected: true, name: "MEMO", mqttProxyConnected: false, mqttUplinkEnabled: true, mqttDownlinkEnabled: true) - ConnectedDevice(deviceConnected: true, name: "MEMO", mqttProxyConnected: true, mqttUplinkEnabled: true, mqttDownlinkEnabled: true, mqttTopic: "msh/US/2/e/#") - ConnectedDevice(deviceConnected: true, name: "MEMO", mqttProxyConnected: false, mqttUplinkEnabled: true, mqttDownlinkEnabled: false) - ConnectedDevice(deviceConnected: true, name: "MEMO", mqttProxyConnected: true, mqttUplinkEnabled: true, mqttDownlinkEnabled: false) - ConnectedDevice(deviceConnected: true, name: "MEMO", mqttProxyConnected: false, mqttUplinkEnabled: false, mqttDownlinkEnabled: true) - ConnectedDevice(deviceConnected: true, name: "MEMO", mqttProxyConnected: true, mqttUplinkEnabled: false, mqttDownlinkEnabled: true) - ConnectedDevice(deviceConnected: true, name: "MEMO", mqttProxyConnected: true) - ConnectedDevice(deviceConnected: false, name: "MEMO", mqttProxyConnected: false) - }.previewLayout(.fixed(width: 150, height: 275)) - } +#Preview("Multiple variants") { + VStack(alignment: .trailing) { + ConnectedDevice(deviceConnected: true, name: "MEMO", mqttProxyConnected: true) + ConnectedDevice(deviceConnected: true, name: "MEMO", mqttProxyConnected: false, mqttUplinkEnabled: true, mqttDownlinkEnabled: true) + ConnectedDevice(deviceConnected: true, name: "MEMO", mqttProxyConnected: true, mqttUplinkEnabled: true, mqttDownlinkEnabled: true, mqttTopic: "msh/US/2/e/#") + ConnectedDevice(deviceConnected: true, name: "MEMO", mqttProxyConnected: false, mqttUplinkEnabled: true, mqttDownlinkEnabled: false) + ConnectedDevice(deviceConnected: true, name: "MEMO", mqttProxyConnected: true, mqttUplinkEnabled: true, mqttDownlinkEnabled: false) + ConnectedDevice(deviceConnected: true, name: "MEMO", mqttProxyConnected: false, mqttUplinkEnabled: false, mqttDownlinkEnabled: true) + ConnectedDevice(deviceConnected: true, name: "MEMO", mqttProxyConnected: true, mqttUplinkEnabled: false, mqttDownlinkEnabled: true) + ConnectedDevice(deviceConnected: true, name: "MEMO", mqttProxyConnected: true) + ConnectedDevice(deviceConnected: false, name: "MEMO", mqttProxyConnected: false) + } + .environmentObject(AccessoryManager.shared) +} + +#Preview("Navigation header item") { + NavigationView { + Text("Connect screen") + .navigationTitle("Connect") + .navigationBarItems( + leading: MeshtasticLogo(), + trailing: ZStack { + ConnectedDevice(deviceConnected: true, name: "MEMO", mqttProxyConnected: false) + .environmentObject(AccessoryManager.shared) + } + ) + } } diff --git a/Meshtastic/Views/Helpers/RXTXIndicatorView.swift b/Meshtastic/Views/Helpers/RXTXIndicatorView.swift index 25168718..860b3734 100644 --- a/Meshtastic/Views/Helpers/RXTXIndicatorView.swift +++ b/Meshtastic/Views/Helpers/RXTXIndicatorView.swift @@ -12,7 +12,7 @@ import OSLog struct RXTXIndicatorWidget: View { @EnvironmentObject var accessoryManager: AccessoryManager @State private var isPopoverOpen = false - + let fontSize: CGFloat = 7.0 var body: some View { Button( action: { @@ -38,7 +38,7 @@ struct RXTXIndicatorWidget: View { #else self.isPopoverOpen.toggle() #endif - + }) { VStack(spacing: 3.0) { HStack(spacing: 2.0) { @@ -102,9 +102,9 @@ struct LEDIndicator: View { @Environment(\.colorScheme) var colorScheme @Binding var flash: Int let color: Color - + @State private var brightness: Double = 0.0 - + var body: some View { Circle() .foregroundColor(color.opacity(brightness)) From c19c810749ac9d7f5c347a87f929b1f321d119a1 Mon Sep 17 00:00:00 2001 From: Mike Robbins Date: Wed, 10 Dec 2025 01:58:41 -0500 Subject: [PATCH 40/68] UpdateCoreData.updateAnyPacketFrom: mirror firmware's lastHeard/snr/rssi/hopsAway update logic from NodeDB::updateFrom (#1492) --- .../Accessory Manager/AccessoryManager.swift | 8 ++ Meshtastic/Persistence/UpdateCoreData.swift | 79 +++++++++++++------ 2 files changed, 62 insertions(+), 25 deletions(-) diff --git a/Meshtastic/Accessory/Accessory Manager/AccessoryManager.swift b/Meshtastic/Accessory/Accessory Manager/AccessoryManager.swift index 5e2e98c8..8cc356e1 100644 --- a/Meshtastic/Accessory/Accessory Manager/AccessoryManager.swift +++ b/Meshtastic/Accessory/Accessory Manager/AccessoryManager.swift @@ -493,6 +493,14 @@ class AccessoryManager: ObservableObject, MqttClientProxyManagerDelegate { handleMyInfo(myNodeInfo) case .packet(let packet): + // All received packets get passed through updateAnyPacketFrom to update lastHeard, rxSnr, etc. (like firmware's NodeDB::updateFrom). + if let connectedNodeNum = self.activeDeviceNum { + updateAnyPacketFrom(packet: packet, activeDeviceNum: connectedNodeNum, context: context) + } else { + Logger.mesh.error("🕸️ Unable to determine connectedNodeNum for updateAnyPacketFrom. Skipping.") + } + + // Dispatch based on packet contents. if case let .decoded(data) = packet.payloadVariant { switch data.portnum { case .textMessageApp, .detectionSensorApp, .alertApp: diff --git a/Meshtastic/Persistence/UpdateCoreData.swift b/Meshtastic/Persistence/UpdateCoreData.swift index 380193a8..c56644a5 100644 --- a/Meshtastic/Persistence/UpdateCoreData.swift +++ b/Meshtastic/Persistence/UpdateCoreData.swift @@ -165,6 +165,60 @@ public func clearCoreDataDatabase(context: NSManagedObjectContext, includeRoutes } } +func updateAnyPacketFrom (packet: MeshPacket, activeDeviceNum: Int64, context: NSManagedObjectContext) { + // Update NodeInfoEntity for any packet received. This mirrors the firmware's NodeDB::updateFrom, which sniffs ALL received packets and updates the radio's nodeDB with packet.from's: + // - last_heard (from rxTime) + // - snr + // - via_mqtt + // - hops_away + + // However, unlike the firmware, this function will NOT create a new NodeInfoEntity if we don't have it already. We'll leave that to the existing code paths. + + // We do NOT update fetchedNode[0].channel, because we may hear a node over multiple channels, and only some packet types should update what we consider the node's channel to be. (Example: primary private channel, secondary public channel. A text message on the secondary public channel should NOT change fetchedNode[0].channel.) + + guard packet.from > 0 else { return } + guard packet.from != activeDeviceNum else { return } // Ignore if packet is from our own node + + let fetchNodeInfoAppRequest = NodeInfoEntity.fetchRequest() + fetchNodeInfoAppRequest.predicate = NSPredicate(format: "num == %lld", Int64(packet.from)) + + do { + let fetchedNode = try context.fetch(fetchNodeInfoAppRequest) + if fetchedNode.count >= 1 { + fetchedNode[0].id = Int64(packet.from) + fetchedNode[0].num = Int64(packet.from) + + if packet.rxTime > 0 { + fetchedNode[0].lastHeard = Date(timeIntervalSince1970: TimeInterval(Int64(packet.rxTime))) + Logger.data.info("💾 [updateAnyPacketFrom] Updating node \(packet.from.toHex(), privacy: .public) lastHeard from rxTime=\(packet.rxTime)") + } else { + fetchedNode[0].lastHeard = Date() + Logger.data.info("💾 [updateAnyPacketFrom] Updating node \(packet.from.toHex(), privacy: .public) lastHeard to now (rxTime==0)") + } + + fetchedNode[0].snr = packet.rxSnr + fetchedNode[0].rssi = packet.rxRssi + fetchedNode[0].viaMqtt = packet.viaMqtt + + if packet.hopStart != 0 && packet.hopLimit <= packet.hopStart { + fetchedNode[0].hopsAway = Int32(packet.hopStart - packet.hopLimit) + Logger.data.info("💾 [updateAnyPacketFrom] Updating node \(packet.from.toHex(), privacy: .public) hopsAway=\(fetchedNode[0].hopsAway)") + } + + do { + try context.save() + Logger.data.info("💾 [updateAnyPacketFrom] Updating node \(fetchedNode[0].num.toHex(), privacy: .public) snr=\(fetchedNode[0].snr), rssi=\(fetchedNode[0].rssi) from packet \(packet.id.toHex(), privacy: .public)") + } catch { + context.rollback() + let nsError = error as NSError + Logger.data.error("💥 [updateAnyPacketFrom] Error Saving node \(fetchedNode[0].num.toHex(), privacy: .public) from packet \(packet.id.toHex(), privacy: .public) \(nsError, privacy: .public)") + } + } + } catch { + Logger.data.error("💥 [updateAnyPacketFrom] fetch data error") + } +} + func upsertNodeInfoPacket (packet: MeshPacket, favorite: Bool = false, context: NSManagedObjectContext) { let logString = String.localizedStringWithFormat("[NodeInfo] received for: %@".localized, packet.from.toHex()) @@ -316,16 +370,6 @@ func upsertNodeInfoPacket (packet: MeshPacket, favorite: Bool = false, context: } else { // Update an existing node - fetchedNode[0].id = Int64(packet.from) - fetchedNode[0].num = Int64(packet.from) - if packet.rxTime > 0 { - fetchedNode[0].lastHeard = Date(timeIntervalSince1970: TimeInterval(Int64(packet.rxTime))) - } else { - fetchedNode[0].lastHeard = Date() - } - fetchedNode[0].snr = packet.rxSnr - fetchedNode[0].rssi = packet.rxRssi - fetchedNode[0].viaMqtt = packet.viaMqtt if packet.to == Constants.maximumNodeNum || packet.to == UserDefaults.preferredPeripheralNum { fetchedNode[0].channel = Int32(packet.channel) } @@ -463,22 +507,7 @@ func upsertPositionPacket (packet: MeshPacket, context: NSManagedObjectContext) mutablePositions.removeAllObjects() } mutablePositions.add(position) - fetchedNode[0].id = Int64(packet.from) - fetchedNode[0].num = Int64(packet.from) - // Update the node's lastHeard. - // Some misconfigured nodes will broadcast position packets that claim GPS timestamps in the future. When updating lastHeard, don't use any future timestamps: fallback to using rxTime or Date() instead. - if positionMessage.time > 0 && (Date(timeIntervalSince1970: TimeInterval(Int64(positionMessage.time))) <= Date()) { - fetchedNode[0].lastHeard = Date(timeIntervalSince1970: TimeInterval(Int64(positionMessage.time))) - } else if packet.rxTime > 0 { - fetchedNode[0].lastHeard = Date(timeIntervalSince1970: TimeInterval(Int64(packet.rxTime))) - } else { - fetchedNode[0].lastHeard = Date() - } - - fetchedNode[0].snr = packet.rxSnr - fetchedNode[0].rssi = packet.rxRssi - fetchedNode[0].viaMqtt = packet.viaMqtt fetchedNode[0].channel = Int32(packet.channel) fetchedNode[0].positions = mutablePositions.copy() as? NSOrderedSet From 865e5e950ba5cce52d129639f9210fcf844bc266 Mon Sep 17 00:00:00 2001 From: Mike Robbins Date: Thu, 11 Dec 2025 00:27:44 -0500 Subject: [PATCH 41/68] `CLIENT_BASE` add-favorite/role-change confirmation dialog (#1493) * FavoriteNodeButton: refactor task out * AccessoryManager.connectedDeviceRole helper * FavoriteNodeButton: show confirmation dialog when a CLIENT_BASE is trying to add a favorite * addContactFromURL: add comment referencing upcoming change in https://github.com/meshtastic/firmware/pull/8495 * DeviceConfig: role picker: show a warning when selecting CLIENT_BASE, similar to warning shown for ROUTER * Adjust device configuration Client Base warning text --- Localizable.xcstrings | 8 ++ .../AccessoryManager+ToRadio.swift | 1 + .../Accessory Manager/AccessoryManager.swift | 7 ++ .../Helpers/Actions/FavoriteNodeButton.swift | 83 ++++++++++++------- .../Views/Settings/Config/DeviceConfig.swift | 24 ++++-- 5 files changed, 89 insertions(+), 34 deletions(-) diff --git a/Localizable.xcstrings b/Localizable.xcstrings index 0c8fab99..d4d38083 100644 --- a/Localizable.xcstrings +++ b/Localizable.xcstrings @@ -7662,6 +7662,10 @@ } } }, + "Client Base should only favorite other nodes you control. Improper use will hurt your local mesh." : { + "comment" : "A message displayed in a confirmation dialog when trying to favorite a node as a CLIENT_BASE.", + "isCommentAutoGenerated" : true + }, "Client Hidden" : { "localizations" : { "de" : { @@ -41885,6 +41889,10 @@ } } }, + "Yes, I control this node" : { + "comment" : "A button label that appears in a confirmation sheet when favoriting a node as a CLIENT_BASE.", + "isCommentAutoGenerated" : true + }, "Yesterday" : { "localizations" : { "de" : { diff --git a/Meshtastic/Accessory/Accessory Manager/AccessoryManager+ToRadio.swift b/Meshtastic/Accessory/Accessory Manager/AccessoryManager+ToRadio.swift index ca219430..7c928b3d 100644 --- a/Meshtastic/Accessory/Accessory Manager/AccessoryManager+ToRadio.swift +++ b/Meshtastic/Accessory/Accessory Manager/AccessoryManager+ToRadio.swift @@ -165,6 +165,7 @@ extension AccessoryManager { nodeMeshPacket.decoded = dataNodeMessage // Update local database with the new node info + // FUTURE: after https://github.com/meshtastic/firmware/pull/8495 is merged, `favorite: true` becomes `favorite: (connectedDeviceRole != DeviceRoles.clientBase)` upsertNodeInfoPacket(packet: nodeMeshPacket, favorite: true, context: context) } } catch { diff --git a/Meshtastic/Accessory/Accessory Manager/AccessoryManager.swift b/Meshtastic/Accessory/Accessory Manager/AccessoryManager.swift index 8cc356e1..2dd5fb35 100644 --- a/Meshtastic/Accessory/Accessory Manager/AccessoryManager.swift +++ b/Meshtastic/Accessory/Accessory Manager/AccessoryManager.swift @@ -706,6 +706,13 @@ extension AccessoryManager { return activeConnection?.device.firmwareVersion } + var connectedDeviceRole: DeviceRoles? { + guard let connectedNodeNum = activeDeviceNum else { return nil } + guard let connectedNode = getNodeInfo(id: connectedNodeNum, context: context) else { return nil } + guard let connectedNodeUser = connectedNode.user else { return nil } + return DeviceRoles(rawValue: Int(connectedNodeUser.role)) + } + func checkIsVersionSupported(forVersion: String) -> Bool { let myVersion = connectedVersion ?? "0.0.0" let supportedVersion = UserDefaults.firmwareVersion == "0.0.0" || diff --git a/Meshtastic/Views/Nodes/Helpers/Actions/FavoriteNodeButton.swift b/Meshtastic/Views/Nodes/Helpers/Actions/FavoriteNodeButton.swift index 4ca8009b..83bac1d3 100644 --- a/Meshtastic/Views/Nodes/Helpers/Actions/FavoriteNodeButton.swift +++ b/Meshtastic/Views/Nodes/Helpers/Actions/FavoriteNodeButton.swift @@ -8,39 +8,20 @@ struct FavoriteNodeButton: View { @Environment(\.managedObjectContext) var context @ObservedObject var node: NodeInfoEntity + @State var isShowingClientBaseConfirmation = false var body: some View { + let connectedRoleIsClientBase = accessoryManager.connectedDeviceRole == DeviceRoles.clientBase Button { + // Special case for CLIENT_BASE: show confirmation when attempting to favorite a node + if connectedRoleIsClientBase && !node.favorite { + isShowingClientBaseConfirmation = true + return + } + // Normal case: perform action immediately guard let connectedNodeNum = accessoryManager.activeDeviceNum else { return } Task { - do { - if node.favorite { - try await accessoryManager.removeFavoriteNode( - node: node, - connectedNodeNum: Int64(connectedNodeNum) - ) - } else { - try await accessoryManager.setFavoriteNode( - node: node, - connectedNodeNum: Int64(connectedNodeNum) - ) - } - - Task { @MainActor in - // Update CoreData - node.favorite = !node.favorite - - do { - try context.save() - } catch { - context.rollback() - Logger.data.error("Save Node Favorite Error") - } - Logger.data.debug("Favorited a node") - } - } catch { - - } + await assignFavorite(node: node, setToFavorite: !node.favorite, connectedNodeNum: Int64(connectedNodeNum)) } } label: { Label { @@ -50,5 +31,51 @@ struct FavoriteNodeButton: View { .symbolRenderingMode(.multicolor) } } + .confirmationDialog( + "Are you sure?", + isPresented: $isShowingClientBaseConfirmation, + titleVisibility: .visible + ) { + Button("Yes, I control this node") { + guard let connectedNodeNum = accessoryManager.activeDeviceNum else { return } + Task { + await assignFavorite(node: node, setToFavorite: true, connectedNodeNum: Int64(connectedNodeNum)) + } + } + Button("Cancel", role: .cancel) { } + } message: { + Text("Client Base should only favorite other nodes you control. Improper use will hurt your local mesh.") + } + } + + private func assignFavorite (node: NodeInfoEntity, setToFavorite: Bool, connectedNodeNum: Int64) async { + do { + if setToFavorite { + try await accessoryManager.setFavoriteNode( + node: node, + connectedNodeNum: Int64(connectedNodeNum) + ) + } else { + try await accessoryManager.removeFavoriteNode( + node: node, + connectedNodeNum: Int64(connectedNodeNum) + ) + } + + Task { @MainActor in + // Update CoreData + node.favorite = setToFavorite + + do { + try context.save() + } catch { + context.rollback() + Logger.data.error("Save Node Favorite Error") + } + Logger.data.debug("Favorited a node") + } + } catch { + + } } } diff --git a/Meshtastic/Views/Settings/Config/DeviceConfig.swift b/Meshtastic/Views/Settings/Config/DeviceConfig.swift index 80fe3dba..a03c8a22 100644 --- a/Meshtastic/Views/Settings/Config/DeviceConfig.swift +++ b/Meshtastic/Views/Settings/Config/DeviceConfig.swift @@ -29,8 +29,9 @@ struct DeviceConfig: View { @State var ledHeartbeatEnabled = true @State var tripleClickAsAdHocPing = true @State var tzdef = "" - @State private var showRouterWarning = false - + @State private var showSpecialRoleWarning = false + @State private var showSpecialRoleWarningForRole: Int = 0 + var body: some View { Form { ConfigHeader(title: "Device", config: \.deviceConfig, node: node, onAppear: setDeviceValues) @@ -43,13 +44,14 @@ struct DeviceConfig: View { } } .onChange(of: deviceRole) { _, newRole in - if hasChanges && [DeviceRoles.router.rawValue, DeviceRoles.routerLate.rawValue].contains(newRole) { - showRouterWarning = true + if hasChanges && [DeviceRoles.router.rawValue, DeviceRoles.routerLate.rawValue, DeviceRoles.clientBase.rawValue].contains(newRole) { + showSpecialRoleWarningForRole = newRole + showSpecialRoleWarning = true } } .confirmationDialog( "Are you sure?", - isPresented: $showRouterWarning, + isPresented: $showSpecialRoleWarning, titleVisibility: .visible ) { @@ -60,7 +62,7 @@ struct DeviceConfig: View { setDeviceValues() } } message: { - Text("The Router roles are only for high vantage locations like mountaintops and towers with few nearby nodes, not for use in urban areas. Improper use will hurt your local mesh.") + Text(specialRoleWarningMessage(newRole: showSpecialRoleWarningForRole)) } Text(DeviceRoles(rawValue: deviceRole)?.description ?? "") .foregroundColor(.gray) @@ -332,4 +334,14 @@ struct DeviceConfig: View { self.tzdef = node?.deviceConfig?.tzdef ?? "" hasChanges = false } + + private func specialRoleWarningMessage(newRole: Int) -> String { + if [DeviceRoles.router.rawValue, DeviceRoles.routerLate.rawValue].contains(newRole) { + return "The Router roles are only for high vantage locations like mountaintops and towers with few nearby nodes, not for use in urban areas. Improper use will hurt your local mesh." + } else if newRole == DeviceRoles.clientBase.rawValue { + return "Switching to Client Base will clear this node's favorites. Client Base should only favorite other nodes you control. Improper use will hurt your local mesh." + } else { + return "" + } + } } From 8346fb8073b5df47c97f80c4b0876a7e33d1a41b Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Mon, 15 Dec 2025 11:15:54 -0800 Subject: [PATCH 42/68] Compass view (#1521) * Added compass view * Added Compass View * Node colors in compass * Update Muzi R1 Neo to actively supported * Update PositionPopover.swift Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Update CompassView.swift Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Update CompassView.swift Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Update Meshtastic/Views/Helpers/CompassView.swift Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Update Meshtastic/Views/Helpers/CompassView.swift Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --------- Co-authored-by: Benjamin Faershtein <119711889+RCGV1@users.noreply.github.com> Co-authored-by: Jonathan Bennett Co-authored-by: Ben Meadors Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- Localizable.xcstrings | 17 +- Meshtastic.xcodeproj/project.pbxproj | 4 + .../xcshareddata/swiftpm/Package.resolved | 2 +- Meshtastic/Helpers/LocationsHandler.swift | 39 +++ Meshtastic/Views/Helpers/CompassView.swift | 295 ++++++++++++++++++ .../Nodes/Helpers/Map/PositionPopover.swift | 32 +- .../Views/Nodes/Helpers/NodeDetail.swift | 15 + 7 files changed, 398 insertions(+), 6 deletions(-) create mode 100644 Meshtastic/Views/Helpers/CompassView.swift diff --git a/Localizable.xcstrings b/Localizable.xcstrings index d4d38083..4114740d 100644 --- a/Localizable.xcstrings +++ b/Localizable.xcstrings @@ -5135,6 +5135,12 @@ } } } + }, + "Bearing: %@" : { + + }, + "Bearing: N/A" : { + }, "Biking" : { "localizations" : { @@ -8085,6 +8091,9 @@ } } } + }, + "Compass" : { + }, "Config" : { "localizations" : { @@ -11995,6 +12004,9 @@ } } } + }, + "Distance: %@" : { + }, "Documentation" : { "localizations" : { @@ -24767,6 +24779,9 @@ } } } + }, + "Open Compass" : { + }, "Open Settings" : { "localizations" : { @@ -42306,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 83759679..b87ce5f2 100644 --- a/Meshtastic.xcodeproj/project.pbxproj +++ b/Meshtastic.xcodeproj/project.pbxproj @@ -98,6 +98,7 @@ B3E905B12B71F7F300654D07 /* TextMessageField.swift in Sources */ = {isa = PBXBuildFile; fileRef = B3E905B02B71F7F300654D07 /* TextMessageField.swift */; }; BC10380F2DD4334400B00BFA /* AddContactIntent.swift in Sources */ = {isa = PBXBuildFile; fileRef = BC10380E2DD4333C00B00BFA /* AddContactIntent.swift */; }; BC6B45FF2CB2F98900723CEB /* SaveChannelSettingsIntent.swift in Sources */ = {isa = PBXBuildFile; fileRef = BC6B45FE2CB2F98900723CEB /* SaveChannelSettingsIntent.swift */; }; + BCA9A82C2EC802CF00166292 /* CompassView.swift in Sources */ = {isa = PBXBuildFile; fileRef = BCA9A82B2EC802CF00166292 /* CompassView.swift */; }; BCB35B4F2E5FC42500B04F60 /* MessageNodeIntent.swift in Sources */ = {isa = PBXBuildFile; fileRef = BCB35B4E2E5FC41E00B04F60 /* MessageNodeIntent.swift */; }; BCB613812C67290800485544 /* SendWaypointIntent.swift in Sources */ = {isa = PBXBuildFile; fileRef = BCB613802C67290800485544 /* SendWaypointIntent.swift */; }; BCB613832C672A2600485544 /* MessageChannelIntent.swift in Sources */ = {isa = PBXBuildFile; fileRef = BCB613822C672A2600485544 /* MessageChannelIntent.swift */; }; @@ -409,6 +410,7 @@ B3E905B02B71F7F300654D07 /* TextMessageField.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TextMessageField.swift; sourceTree = ""; }; BC10380E2DD4333C00B00BFA /* AddContactIntent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddContactIntent.swift; sourceTree = ""; }; BC6B45FE2CB2F98900723CEB /* SaveChannelSettingsIntent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SaveChannelSettingsIntent.swift; sourceTree = ""; }; + BCA9A82B2EC802CF00166292 /* CompassView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CompassView.swift; sourceTree = ""; }; BCB35B4E2E5FC41E00B04F60 /* MessageNodeIntent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageNodeIntent.swift; sourceTree = ""; }; BCB613802C67290800485544 /* SendWaypointIntent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SendWaypointIntent.swift; sourceTree = ""; }; BCB613822C672A2600485544 /* MessageChannelIntent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageChannelIntent.swift; sourceTree = ""; }; @@ -1274,6 +1276,7 @@ 8D3F8A3E2D44BB02009EAAA4 /* PowerMetrics.swift */, 237B46952DC8F1C100B22D99 /* RateLimitedButton.swift */, 23A1AFB62E42BD2500E46C96 /* RXTXIndicatorView.swift */, + BCA9A82B2EC802CF00166292 /* CompassView.swift */, ); path = Helpers; sourceTree = ""; @@ -1814,6 +1817,7 @@ 233E99BE2D849D3200CC3A77 /* RadiationCompactWidget.swift in Sources */, DDD94A502845C8F5004A87A0 /* DateTimeText.swift in Sources */, DDB6ABE228B13FB500384BA1 /* PositionConfigEnums.swift in Sources */, + BCA9A82C2EC802CF00166292 /* CompassView.swift in Sources */, DD994B69295F88B60013760A /* IntervalEnums.swift in Sources */, 23D316932E5618D2002FA4FB /* AsyncGate.swift in Sources */, 23FF00B62E323C75001DF095 /* AccessoryManager+Connect.swift in Sources */, diff --git a/Meshtastic.xcworkspace/xcshareddata/swiftpm/Package.resolved b/Meshtastic.xcworkspace/xcshareddata/swiftpm/Package.resolved index 53375016..d0b85617 100644 --- a/Meshtastic.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/Meshtastic.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -1,5 +1,5 @@ { - "originHash" : "fd71b247ba909b0eb360db5530e1068363839c5e169dea6f6a9974b2d98276f4", + "originHash" : "0fb5b226f8ca0b357ce7816ebac09d017cbe0ad253452876e5e2ff8e555c7124", "pins" : [ { "identity" : "cocoamqtt", diff --git a/Meshtastic/Helpers/LocationsHandler.swift b/Meshtastic/Helpers/LocationsHandler.swift index b174970a..6d44499d 100644 --- a/Meshtastic/Helpers/LocationsHandler.swift +++ b/Meshtastic/Helpers/LocationsHandler.swift @@ -26,6 +26,8 @@ import OSLog @Published var recordingStarted: Date? @Published var distanceTraveled = 0.0 @Published var elevationGain = 0.0 + @Published var heading: Double = 0.0 // Current heading in degrees + @Published var headingUpdatesStarted: Bool = false // Track heading updates state @Published var updatesStarted: Bool = UserDefaults.standard.bool(forKey: "liveUpdatesStarted") { @@ -131,6 +133,10 @@ import OSLog self.manager.desiredAccuracy = kCLLocationAccuracyBest // Set the distance filter to only receive updates when the device has moved a certain distance. self.manager.distanceFilter = kCLDistanceFilterNone // Receive all updates initially + if CLLocationManager.headingAvailable() { + self.manager.headingFilter = 1 // Update heading when it changes by 1 degree + self.manager.headingOrientation = .portrait // Adjust based on device orientation + } } func startLocationUpdates() { @@ -178,6 +184,39 @@ import OSLog // The Task completes implicitly here. } } + + // New method to start heading updates + func startHeadingUpdates() { + guard CLLocationManager.headingAvailable() else { + Logger.services.warning("📍 [App] Heading updates not available on this device.") + return + } + + guard manager.authorizationStatus == .authorizedAlways || manager.authorizationStatus == .authorizedWhenInUse else { + Logger.services.warning("📍 [App] Cannot start heading updates: insufficient authorization status.") + return + } + + Logger.services.info("📍 [App] Starting heading updates") + manager.startUpdatingHeading() + headingUpdatesStarted = true + } + + // New method to stop heading updates + func stopHeadingUpdates() { + Logger.services.info("🛑 [App] Stopping heading updates") + manager.stopUpdatingHeading() + headingUpdatesStarted = false + } + + // Implement the CLLocationManagerDelegate method for heading updates + func locationManager(_ manager: CLLocationManager, didUpdateHeading newHeading: CLHeading) { + // Update heading on the main thread + Task { @MainActor in + self.heading = newHeading.trueHeading >= 0 ? newHeading.trueHeading : newHeading.magneticHeading + } + } + /// Stops receiving live location updates. func stopLocationUpdates() { Logger.services.info("🛑 [App] Stopping location updates") diff --git a/Meshtastic/Views/Helpers/CompassView.swift b/Meshtastic/Views/Helpers/CompassView.swift new file mode 100644 index 00000000..1e58b224 --- /dev/null +++ b/Meshtastic/Views/Helpers/CompassView.swift @@ -0,0 +1,295 @@ +// +// CompassView.swift +// Meshtastic +// +// Created by Benjamin Faershtein on 11/14/25. +// + +import SwiftUI +import CoreLocation +import UIKit + +struct CompassView: View { + + /// Single waypoint parameter + let waypointLocation: CLLocationCoordinate2D? + + let waypointName: String? + + let color: Color + + @ObservedObject private var locationsHandler = LocationsHandler.shared + + // Haptic alignment tracking + private let alignmentTolerance: Double = 5.0 + @State private var inAlignment = false + + // Compute bearing from user → waypoint + private func bearingToWaypoint() -> Double? { + guard + let waypoint = waypointLocation, + let user = LocationsHandler.currentLocation + else { return nil } + + return BearingCalculator.bearingBetween( + userLocation: user, + waypoint: waypoint + ) + } + + // Trigger a vibration if aligned with waypoint + private func checkAlignment(bearing: Double,heading: Double) { + // Compute minimal angular difference between heading and bearing in [0, 180] + let rawDiff = abs(heading - bearing).truncatingRemainder(dividingBy: 360) + let diff = min(rawDiff, 360 - rawDiff) + + if diff <= alignmentTolerance { + if !inAlignment { + inAlignment = true + let generator = UIImpactFeedbackGenerator(style: .heavy) + generator.impactOccurred() + } + } else { + inAlignment = false + } + } + + + private func distanceToWaypoint() -> CLLocationDistance? { + guard + let waypoint = waypointLocation, + let user = LocationsHandler.currentLocation + else { return nil } + + let userLocation = CLLocation(latitude: user.latitude, longitude: user.longitude) + let waypointLocation = CLLocation(latitude: waypoint.latitude, longitude: waypoint.longitude) + + return userLocation.distance(from: waypointLocation) + } + + // Format distance with localization + private func formatDistance(_ distance: CLLocationDistance) -> String { + let measurement = Measurement(value: distance, unit: UnitLength.meters) + let formatter = MeasurementFormatter() + formatter.unitOptions = .naturalScale + formatter.numberFormatter.maximumFractionDigits = 2 + return formatter.string(from: measurement) + } + + + var body: some View { + NavigationStack { + VStack(spacing: 15) { + + VStack(spacing: 8) { + Text(waypointName ?? "Waypoint") + .font(.title2) + .bold() + .foregroundColor(color) + + if let wp = waypointLocation { + HStack{ + Image(systemName: "mappin.and.ellipse") + Text("\(String(format: "%.4f", wp.latitude)), \(String(format: "%.4f", wp.longitude))") + .font(.subheadline) + } + + if let distance = distanceToWaypoint() { + HStack{ + Image(systemName: "lines.measurement.horizontal") + Text("Distance: \(formatDistance(distance))") + .font(.subheadline) + .fontWeight(.semibold) + } + } + HStack { + Image(systemName: "location.north") + if let bearing = bearingToWaypoint() { + Text("Bearing: \(String(format: "%.0f°", bearing))") + .font(.subheadline) + } else { + Text("Bearing: N/A") + .font(.subheadline) + } + } + } + } + .padding() + + Capsule() + .frame(width: 5, height: 50) + ZStack { + + // Cardinal/degree markers + ForEach(Marker.markers(), id: \.self) { marker in + CompassMarkerView( + marker: marker, + compassDegrees: -locationsHandler.heading + ) + } + + // Waypoint bearing indicator + if let bearing = bearingToWaypoint() { + WaypointMarkerView( + bearing: bearing, + compassDegrees: locationsHandler.heading, + color: color + ) + // Move waypoint marker outside compass + .onChange(of: locationsHandler.heading) { _, _ in + checkAlignment(bearing: bearing,heading:locationsHandler.heading) + } + } + + } + .frame(width: 300, height: 300) + .rotationEffect(Angle(degrees: -locationsHandler.heading)) + .statusBar(hidden: true) + .onAppear { + locationsHandler.startHeadingUpdates() + locationsHandler.startLocationUpdates() + } + .onDisappear { + locationsHandler.stopHeadingUpdates() + locationsHandler.stopLocationUpdates() + } + .navigationTitle("Compass") + } + } + } +} + + +// MARK: - Waypoint Marker View + +struct WaypointMarkerView: View { + let bearing: Double + let compassDegrees: Double + let color: Color + + var body: some View { + Circle() + .frame(width: 20, height: 20) + .foregroundColor(color) + .offset(y: -170) + .rotationEffect(Angle(degrees: bearing)) + } + +} + + +// MARK: - Bearing Calculator + +struct BearingCalculator { + + static func bearingBetween( + userLocation: CLLocationCoordinate2D, + waypoint: CLLocationCoordinate2D + ) -> Double { + + let lat1 = userLocation.latitude * .pi / 180 + let lon1 = userLocation.longitude * .pi / 180 + let lat2 = waypoint.latitude * .pi / 180 + let lon2 = waypoint.longitude * .pi / 180 + + let dLon = lon2 - lon1 + + let y = sin(dLon) * cos(lat2) + let x = cos(lat1) * sin(lat2) + - sin(lat1) * cos(lat2) * cos(dLon) + + var bearing = atan2(y, x) * 180 / .pi + if bearing < 0 { bearing += 360 } + + return bearing + } +} + + +// MARK: - Marker Model + +struct Marker: Hashable { + let degrees: Double + let label: String + + init(degrees: Double, label: String = "") { + self.degrees = degrees + self.label = label + } + + func degreeText() -> String { + return String(format: "%.0f", self.degrees) + } + + static func markers() -> [Marker] { + return [ + Marker(degrees: 0, label: "N"), + Marker(degrees: 30), + Marker(degrees: 60), + Marker(degrees: 90, label: "E"), + Marker(degrees: 120), + Marker(degrees: 150), + Marker(degrees: 180, label: "S"), + Marker(degrees: 210), + Marker(degrees: 240), + Marker(degrees: 270, label: "W"), + Marker(degrees: 300), + Marker(degrees: 330) + ] + } +} + + +// MARK: - Compass Marker View + +struct CompassMarkerView: View { + let marker: Marker + let compassDegrees: Double + + var body: some View { + VStack { + Text(marker.degreeText()) + .fontWeight(.light) + .rotationEffect(textAngle()) + + Capsule() + .frame(width: capsuleWidth(), height: capsuleHeight()) + .foregroundColor(capsuleColor()) + + Text(marker.label) + .fontWeight(.bold) + .rotationEffect(textAngle()) + .padding(.bottom, 180) + } + .rotationEffect(Angle(degrees: marker.degrees)) + } + + private func capsuleWidth() -> CGFloat { + marker.degrees == 0 ? 7 : 3 + } + + private func capsuleHeight() -> CGFloat { + marker.degrees == 0 ? 45 : 30 + } + + private func capsuleColor() -> Color { + marker.degrees == 0 ? .red : .gray + } + + private func textAngle() -> Angle { + Angle(degrees: -compassDegrees - marker.degrees) + } +} + + +// MARK: - Preview + +struct CompassView_Previews: PreviewProvider { + static var previews: some View { + CompassView( + waypointLocation: CLLocationCoordinate2D(latitude: 37.3346, longitude: -122.0090), + waypointName: "Apple Park", + color: Color.orange + ) + } +} diff --git a/Meshtastic/Views/Nodes/Helpers/Map/PositionPopover.swift b/Meshtastic/Views/Nodes/Helpers/Map/PositionPopover.swift index 2e5e2809..8ab7b29f 100644 --- a/Meshtastic/Views/Nodes/Helpers/Map/PositionPopover.swift +++ b/Meshtastic/Views/Nodes/Helpers/Map/PositionPopover.swift @@ -19,6 +19,9 @@ struct PositionPopover: View { var position: PositionEntity var popover: Bool = true let distanceFormatter = MKDistanceFormatter() + + @State private var detentSelection: PresentationDetent = .fraction(0.65) + @State private var navigateToCompass = false var body: some View { // Node Color from node.num @@ -42,6 +45,19 @@ struct PositionPopover: View { Divider() HStack(alignment: .center) { VStack(alignment: .leading) { + Button { + detentSelection = .large + DispatchQueue.main.asyncAfter(deadline: .now() + 0.3) { + navigateToCompass = true + } + } label: { + HStack { + Image(systemName: "safari") + Text("Open Compass") + } + } + .padding(.bottom, 5) + /// Time Label { if idiom != .phone { @@ -131,6 +147,7 @@ struct PositionPopover: View { } .padding(.bottom, 5) } + /// Heading let degrees = Angle.degrees(Double(position.heading)) Label { @@ -234,10 +251,17 @@ struct PositionPopover: View { #endif } } + .presentationDetents([.fraction(0.65), .large], selection: $detentSelection) + .presentationContentInteraction(.scrolls) + .presentationDragIndicator(.visible) + .presentationBackgroundInteraction(.enabled(upThrough: .large)) + .navigationDestination(isPresented: $navigateToCompass) { + CompassView( + waypointLocation: position.coordinate, + waypointName: position.nodePosition?.user?.longName ?? "Unknown node", + color: (position.nodePosition?.user?.num != nil && position.nodePosition?.user?.num != 0) ? Color(UIColor(hex: UInt32(position.nodePosition!.user!.num))) : .orange + ) + } } - .presentationDetents([.fraction(0.65), .large]) - .presentationContentInteraction(.scrolls) - .presentationDragIndicator(.visible) - .presentationBackgroundInteraction(.enabled(upThrough: .large)) } } diff --git a/Meshtastic/Views/Nodes/Helpers/NodeDetail.swift b/Meshtastic/Views/Nodes/Helpers/NodeDetail.swift index 69e6ee4e..dc394f35 100644 --- a/Meshtastic/Views/Nodes/Helpers/NodeDetail.swift +++ b/Meshtastic/Views/Nodes/Helpers/NodeDetail.swift @@ -27,6 +27,7 @@ struct NodeDetail: View { var connectedNode: NodeInfoEntity? @ObservedObject var node: NodeInfoEntity @State private var environmentSectionHeight: CGFloat = 0 + @State var showingCompassSheet = false var body: some View { NavigationStack { @@ -473,6 +474,17 @@ struct NodeDetail: View { ) } if node.hasPositions { + #if !targetEnvironment(macCatalyst) + Button { + showingCompassSheet = true + } label: { + Label { + Text("Open Compass") + } icon: { + Image(systemName: "safari") + } + } + #endif NavigateToButton(node: node) } IgnoreNodeButton( @@ -559,6 +571,9 @@ struct NodeDetail: View { } } } + .sheet(isPresented: $showingCompassSheet) { + CompassView(waypointLocation: node.latestPosition?.nodeCoordinate ?? nil, waypointName: node.user?.longName ?? nil, color: Color(UIColor(hex: UInt32(node.num)))) + } .onAppear { scrollView.scrollTo("topOfList", anchor: .top) } From 14efa4cbba97ec13a3c869217d89087a998977a7 Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Tue, 16 Dec 2025 06:26:43 -0800 Subject: [PATCH 43/68] Remove discovery queue --- .../Bluetooth Low Energy/BLETransport.swift | 50 +++++++------------ 1 file changed, 19 insertions(+), 31 deletions(-) diff --git a/Meshtastic/Accessory/Transports/Bluetooth Low Energy/BLETransport.swift b/Meshtastic/Accessory/Transports/Bluetooth Low Energy/BLETransport.swift index 12c9e33c..aa1a32d4 100644 --- a/Meshtastic/Accessory/Transports/Bluetooth Low Energy/BLETransport.swift +++ b/Meshtastic/Accessory/Transports/Bluetooth Low Energy/BLETransport.swift @@ -164,39 +164,27 @@ class BLETransport: Transport { } } - private let peripheralsQueue = DispatchQueue(label: "com.meshtastic.peripheralAccess", qos: .default) - func didDiscover(peripheral: CBPeripheral, rssi: NSNumber) { - guard !restoreInProgress else { return } - - // Use the queue to ensure thread-safe access to the dictionary - peripheralsQueue.async { - let id = peripheral.identifier - let isNew = self.discoveredPeripherals[id] == nil - - // Update the dictionary - if isNew { - self.discoveredPeripherals[id] = (peripheral, Date()) - } else { - self.discoveredPeripherals[id]?.lastSeen = Date() - } - - let device = Device(id: id, - name: peripheral.name ?? "Unknown", - transportType: .ble, - identifier: id.uuidString, - rssi: rssi.intValue) - - // Safely yield results - if isNew { - Logger.transport.debug("🛜 [BLE] Did Discover new device: \(peripheral.name ?? "Unknown", privacy: .public) (\(peripheral.identifier, privacy: .public))") - self.discoveredDeviceContinuation?.yield(.deviceFound(device)) - } else { - let rssiVal = rssi.intValue - let deviceId = id - self.discoveredDeviceContinuation?.yield(.deviceReportedRssi(deviceId, rssiVal)) - } + + let id = peripheral.identifier + let isNew = discoveredPeripherals[id] == nil + if isNew { + discoveredPeripherals[id] = (peripheral, Date()) + } + let device = Device(id: id, + name: peripheral.name ?? "Unknown", + transportType: .ble, + identifier: id.uuidString, + rssi: rssi.intValue) + if isNew { + Logger.transport.debug("🛜 [BLE] Did Discover new device: \(peripheral.name ?? "Unknown", privacy: .public) (\(peripheral.identifier, privacy: .public))") + discoveredDeviceContinuation?.yield(.deviceFound(device)) + } else { + let rssiVal = rssi.intValue + let deviceId = id + discoveredPeripherals[id]?.lastSeen = Date() + discoveredDeviceContinuation?.yield(.deviceReportedRssi(deviceId, rssiVal)) } } From fe1d1d6c3fc0f67d3d8d6d916b47ae0cf9679173 Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Tue, 16 Dec 2025 06:33:08 -0800 Subject: [PATCH 44/68] revert problematic retry functionalliy --- .../Accessory Manager/AccessoryManager+Connect.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Meshtastic/Accessory/Accessory Manager/AccessoryManager+Connect.swift b/Meshtastic/Accessory/Accessory Manager/AccessoryManager+Connect.swift index 84f0ba49..d5ca0929 100644 --- a/Meshtastic/Accessory/Accessory Manager/AccessoryManager+Connect.swift +++ b/Meshtastic/Accessory/Accessory Manager/AccessoryManager+Connect.swift @@ -267,7 +267,7 @@ actor SequentialSteps { var isRunning: Bool = false var externalError: Error? - init(maxRetries: Int = 1, retryDelay: Duration = .seconds(5), @StepsBuilder _ builder: () -> [Step]) { + init(maxRetries: Int = 3, retryDelay: Duration = .seconds(3), @StepsBuilder _ builder: () -> [Step]) { self.maxRetries = maxRetries self.retryDelay = retryDelay self.steps = builder() @@ -353,7 +353,7 @@ actor SequentialSteps { } isRunning = false return - //throw AccessoryError.tooManyRetries + throw AccessoryError.tooManyRetries } func cancel() { From ccee0bfadc33927e56814ae7c73d860bd0a01b20 Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Tue, 16 Dec 2025 06:44:34 -0800 Subject: [PATCH 45/68] format file --- .../CoreData/MessageEntityExtension.swift | 84 +++++++++++-------- 1 file changed, 47 insertions(+), 37 deletions(-) diff --git a/Meshtastic/Extensions/CoreData/MessageEntityExtension.swift b/Meshtastic/Extensions/CoreData/MessageEntityExtension.swift index 5c06ecf2..d6d2c997 100644 --- a/Meshtastic/Extensions/CoreData/MessageEntityExtension.swift +++ b/Meshtastic/Extensions/CoreData/MessageEntityExtension.swift @@ -5,10 +5,9 @@ // Created by Ben on 8/22/23. // -import Foundation - import CoreData import CoreLocation +import Foundation import MapKit import SwiftUI @@ -26,51 +25,62 @@ extension MessageEntity { var tapbacks: [MessageEntity] { let context = PersistenceController.shared.container.viewContext let fetchRequest = MessageEntity.fetchRequest() - fetchRequest.sortDescriptors = [NSSortDescriptor(key: "messageTimestamp", ascending: true)] - fetchRequest.predicate = NSPredicate(format: "replyID == %lld AND isEmoji == true", self.messageId) + fetchRequest.sortDescriptors = [ + NSSortDescriptor(key: "messageTimestamp", ascending: true) + ] + fetchRequest.predicate = NSPredicate( + format: "replyID == %lld AND isEmoji == true", + self.messageId + ) return (try? context.fetch(fetchRequest)) ?? [MessageEntity]() } func displayTimestamp(aboveMessage: MessageEntity?) -> Bool { if let aboveMessage = aboveMessage { - return aboveMessage.timestamp.addingTimeInterval(3600) < timestamp // 60 minutes + return aboveMessage.timestamp.addingTimeInterval(3600) < timestamp // 60 minutes } - return false // First message will have no timestamp + return false // First message will have no timestamp } - - func relayDisplay() -> String? { - guard self.relayNode != 0 else { return nil } - let context = PersistenceController.shared.container.viewContext + func relayDisplay() -> String? { - let relaySuffix = Int64(self.relayNode & 0xFF) - let request: NSFetchRequest = UserEntity.fetchRequest() - request.predicate = NSPredicate(format: "(num & 0xFF) == %lld", relaySuffix) + guard self.relayNode != 0 else { return nil } + let context = PersistenceController.shared.container.viewContext - do { - let users = try context.fetch(request) - - // If exactly one match is found, return its name - if users.count == 1, let name = users.first?.longName, !name.isEmpty { - return "\(name)" - } - - // If no exact match, find the node with the smallest hopsAway - if let closestNode = users.min(by: { lhs, rhs in - guard let lhsHops = lhs.userNode?.hopsAway, let rhsHops = rhs.userNode?.hopsAway else { - return false - } - return lhsHops < rhsHops - }), let name = closestNode.longName, !name.isEmpty { - return "\(name)" - } - - // Fallback to hex node number if no matches - return String(format: "Node 0x%02X", UInt32(self.relayNode & 0xFF)) + let relaySuffix = Int64(self.relayNode & 0xFF) + let request: NSFetchRequest = UserEntity.fetchRequest() + request.predicate = NSPredicate( + format: "(num & 0xFF) == %lld", + relaySuffix + ) - } catch { - return String(format: "Node 0x%02X", UInt32(self.relayNode & 0xFF)) - } - } + do { + let users = try context.fetch(request) + + // If exactly one match is found, return its name + if users.count == 1, let name = users.first?.longName, !name.isEmpty + { + return "\(name)" + } + + // If no exact match, find the node with the smallest hopsAway + if let closestNode = users.min(by: { lhs, rhs in + guard let lhsHops = lhs.userNode?.hopsAway, + let rhsHops = rhs.userNode?.hopsAway + else { + return false + } + return lhsHops < rhsHops + }), let name = closestNode.longName, !name.isEmpty { + return "\(name)" + } + + // Fallback to hex node number if no matches + return String(format: "Node 0x%02X", UInt32(self.relayNode & 0xFF)) + + } catch { + return String(format: "Node 0x%02X", UInt32(self.relayNode & 0xFF)) + } + } } From fcb20cd1acf4fd6c7c27987350aea3635f876947 Mon Sep 17 00:00:00 2001 From: Radio <35003866+radiolee@users.noreply.github.com> Date: Thu, 18 Dec 2025 01:58:15 +0800 Subject: [PATCH 46/68] Update & improve zh-Hans translation (#1523) * Update Muzi R1 Neo to actively supported * update & improve zh-Hans translation rt --------- Co-authored-by: Jonathan Bennett Co-authored-by: Ben Meadors --- Localizable.xcstrings | 86 +++++++++++++++++++++---------------------- 1 file changed, 43 insertions(+), 43 deletions(-) diff --git a/Localizable.xcstrings b/Localizable.xcstrings index 4114740d..33be032d 100644 --- a/Localizable.xcstrings +++ b/Localizable.xcstrings @@ -3854,7 +3854,7 @@ "zh-Hans" : { "stringUnit" : { "state" : "translated", - "value" : "Ambient Lighting module config received: %@" + "value" : "收到环境照明模块配置: %@" } }, "zh-Hant-TW" : { @@ -5461,7 +5461,7 @@ "zh-Hans" : { "stringUnit" : { "state" : "translated", - "value" : "Bluetooth config received: %@" + "value" : "收到蓝牙配置: %@" } }, "zh-Hant-TW" : { @@ -6129,7 +6129,7 @@ "zh-Hans" : { "stringUnit" : { "state" : "translated", - "value" : "Canned Message module config received: %@" + "value" : "收到预设消息模块配置: %@" } }, "zh-Hant-TW" : { @@ -6315,7 +6315,7 @@ "zh-Hans" : { "stringUnit" : { "state" : "translated", - "value" : "Canned Messages Messages Received For: %@" + "value" : "收到预设消息: %@" } }, "zh-Hant-TW" : { @@ -10421,7 +10421,7 @@ "zh-Hans" : { "stringUnit" : { "state" : "translated", - "value" : "Detection Sensor module config received: %@" + "value" : "收到检测传感器模块配置: %@" } }, "zh-Hant-TW" : { @@ -10647,7 +10647,7 @@ "zh-Hans" : { "stringUnit" : { "state" : "translated", - "value" : "Device config received: %@" + "value" : "收到设备配置: %@" } }, "zh-Hant-TW" : { @@ -10831,7 +10831,7 @@ "zh-Hans" : { "stringUnit" : { "state" : "translated", - "value" : "Device Metadata admin message received from: %@" + "value" : "已收到来自设备元数据管理员的消息, 来自: %@" } }, "zh-Hant-TW" : { @@ -11804,7 +11804,7 @@ "zh-Hans" : { "stringUnit" : { "state" : "translated", - "value" : "Display config received: %@" + "value" : "收到显示屏配置: %@" } }, "zh-Hant-TW" : { @@ -11838,7 +11838,7 @@ "zh-Hans" : { "stringUnit" : { "state" : "translated", - "value" : "展示华氏度" + "value" : "显示华氏度" } }, "zh-Hant-TW" : { @@ -12093,7 +12093,7 @@ "zh-Hans" : { "stringUnit" : { "state" : "translated", - "value" : "双击作为按钮" + "value" : "双击代替按钮" } }, "zh-Hant-TW" : { @@ -12429,7 +12429,7 @@ "zh-Hans" : { "stringUnit" : { "state" : "translated", - "value" : "回声" + "value" : "回显" } }, "zh-Hant-TW" : { @@ -13411,7 +13411,7 @@ "zh-Hans" : { "stringUnit" : { "state" : "translated", - "value" : "擦除所有 App 数据?" + "value" : "删除所有 App 数据?" } }, "zh-Hant-TW" : { @@ -13451,7 +13451,7 @@ "zh-Hans" : { "stringUnit" : { "state" : "translated", - "value" : "擦除所有设备和 App 数据?" + "value" : "删除所有设备和 App 数据?" } }, "zh-Hant-TW" : { @@ -14095,7 +14095,7 @@ "zh-Hans" : { "stringUnit" : { "state" : "translated", - "value" : "External Notification module config received: %@" + "value" : "收到外部通知模块配置: %@" } }, "zh-Hant-TW" : { @@ -14712,7 +14712,7 @@ "zh-Hans" : { "stringUnit" : { "state" : "translated", - "value" : "Finish" + "value" : "完成" } }, "zh-Hant-TW" : { @@ -14803,7 +14803,7 @@ "zh-Hans" : { "stringUnit" : { "state" : "translated", - "value" : "固件升级文档" + "value" : "固件升级文件" } }, "zh-Hant-TW" : { @@ -16445,7 +16445,7 @@ "zh-Hans" : { "stringUnit" : { "state" : "translated", - "value" : "GPS Receive GPIO" + "value" : "GPS 接收 GPIO" } }, "zh-Hant-TW" : { @@ -16479,7 +16479,7 @@ "zh-Hans" : { "stringUnit" : { "state" : "translated", - "value" : "GPS Transmit GPIO" + "value" : "GPS 发送 GPIO" } }, "zh-Hant-TW" : { @@ -18001,7 +18001,7 @@ "zh-Hans" : { "stringUnit" : { "state" : "translated", - "value" : "如果难以访问设备的重置按钮,请在此进入 DFU 模式。" + "value" : "如果难以触及设备的重置按钮,请在此进入 DFU 模式。" } }, "zh-Hant-TW" : { @@ -18035,7 +18035,7 @@ "zh-Hans" : { "stringUnit" : { "state" : "translated", - "value" : "如果设置了,您发送的任何数据包都会回传到设备。" + "value" : "如果设置,您发送的任何数据包都会回传到设备。" } }, "zh-Hant-TW" : { @@ -18623,7 +18623,7 @@ "zh-Hans" : { "stringUnit" : { "state" : "translated", - "value" : "倒置顶栏,用于双色显示" + "value" : "倒置顶栏,用于双色显示屏" } }, "zh-Hant-TW" : { @@ -20034,7 +20034,7 @@ "zh-Hans" : { "stringUnit" : { "state" : "translated", - "value" : "LoRa config received: %@" + "value" : "收到 LoRa 配置: %@" } }, "zh-Hant-TW" : { @@ -21218,7 +21218,7 @@ "zh-Hans" : { "stringUnit" : { "state" : "translated", - "value" : "Message received from the text message app." + "value" : "收到来自短信应用的消息。" } }, "zh-Hant-TW" : { @@ -22221,7 +22221,7 @@ "zh-Hans" : { "stringUnit" : { "state" : "translated", - "value" : "MyInfo received: %@" + "value" : "收到我的消息: %@" } }, "zh-Hant-TW" : { @@ -22581,7 +22581,7 @@ "zh-Hans" : { "stringUnit" : { "state" : "translated", - "value" : "Network config received: %@" + "value" : "收到网络配置: %@" } }, "zh-Hant-TW" : { @@ -25770,7 +25770,7 @@ "zh-Hans" : { "stringUnit" : { "state" : "translated", - "value" : "PAX Counter message received for: %@" + "value" : "收到 PAX 计数信息: %@" } }, "zh-Hant-TW" : { @@ -27492,7 +27492,7 @@ "zh-Hans" : { "stringUnit" : { "state" : "translated", - "value" : "Process" + "value" : "处理" } }, "zh-Hant-TW" : { @@ -29274,7 +29274,7 @@ "zh-Hans" : { "stringUnit" : { "state" : "translated", - "value" : "Requested Canned Messages Module Messages for node: %@" + "value" : "请求的节点预设消息模块消息: %@" } }, "zh-Hant-TW" : { @@ -30402,7 +30402,7 @@ "zh-Hans" : { "stringUnit" : { "state" : "translated", - "value" : "Routing received for RequestID: %@ Ack Status: %@" + "value" : "收到请求ID为 %@ 的路由, Ack 状态: %@" } }, "zh-Hant-TW" : { @@ -30550,7 +30550,7 @@ "zh-Hans" : { "stringUnit" : { "state" : "translated", - "value" : "RTTTL Ringtone config received: %@" + "value" : "收到 RTTTL 铃声配置: %@" } }, "zh-Hant-TW" : { @@ -32352,7 +32352,7 @@ "zh-Hans" : { "stringUnit" : { "state" : "translated", - "value" : "Sent a Channel for: %@ Channel Index %d" + "value" : "已发送频道: %@ 频道索引 %d" } }, "zh-Hant-TW" : { @@ -32416,7 +32416,7 @@ "zh-Hans" : { "stringUnit" : { "state" : "translated", - "value" : "Sent a LoRa.Config for: %@" + "value" : "发送 LoRa.Config 给: %@" } }, "zh-Hant-TW" : { @@ -32481,7 +32481,7 @@ "zh-Hans" : { "stringUnit" : { "state" : "translated", - "value" : "Sent a Position Packet from the Apple device GPS to node: %@" + "value" : "从苹果设备 GPS 发送定位数据包给节点: %@" } }, "zh-Hant-TW" : { @@ -32545,7 +32545,7 @@ "zh-Hans" : { "stringUnit" : { "state" : "translated", - "value" : "Sent a Trace Route Request to node: %@" + "value" : "发送跟踪路由请求到节点: %@" } }, "zh-Hant-TW" : { @@ -32609,7 +32609,7 @@ "zh-Hans" : { "stringUnit" : { "state" : "translated", - "value" : "Sent a Waypoint Packet from: %@" + "value" : "发送来自的航点数据包: %@" } }, "zh-Hant-TW" : { @@ -32673,7 +32673,7 @@ "zh-Hans" : { "stringUnit" : { "state" : "translated", - "value" : "Sent message %@ from %@ to %@" + "value" : "发送消息 %@ 来自 %@ 给 %@" } }, "zh-Hant-TW" : { @@ -32995,7 +32995,7 @@ "zh-Hans" : { "stringUnit" : { "state" : "translated", - "value" : "Serial module config received: %@" + "value" : "收到串口模块配置: %@" } }, "zh-Hant-TW" : { @@ -35177,7 +35177,7 @@ "zh-Hans" : { "stringUnit" : { "state" : "translated", - "value" : "Store & Forward module config received: %@" + "value" : "收到存储和转发模块配置: %@" } }, "zh-Hant-TW" : { @@ -35796,7 +35796,7 @@ "zh-Hans" : { "stringUnit" : { "state" : "translated", - "value" : "Telemetry module config received: %@" + "value" : "收到遥测模块配置: %@" } }, "zh-Hant-TW" : { @@ -38051,7 +38051,7 @@ "zh-Hans" : { "stringUnit" : { "state" : "translated", - "value" : "Trace Route request returned: %@" + "value" : "跟踪路由请求回复: %@" } }, "zh-Hant-TW" : { @@ -41081,7 +41081,7 @@ "zh-Hans" : { "stringUnit" : { "state" : "translated", - "value" : "Waypoint Packet received from node: %@" + "value" : "收到航点数据包, 来自节点: %@" } }, "zh-Hant-TW" : { @@ -41270,7 +41270,7 @@ "zh-Hans" : { "stringUnit" : { "state" : "translated", - "value" : "锁意味着什么?" + "value" : "锁头图标含义" } }, "zh-Hant-TW" : { @@ -41310,7 +41310,7 @@ "zh-Hans" : { "stringUnit" : { "state" : "translated", - "value" : "什么是 Meshtastic?" + "value" : "Meshtastic 是什么?" } }, "zh-Hant-TW" : { From 9b6e645bd978e859b977c412fa253f6d447cddc6 Mon Sep 17 00:00:00 2001 From: Ben Meadors Date: Wed, 17 Dec 2025 20:26:44 -0600 Subject: [PATCH 47/68] Update protobufs to 2.7.1 --- .../xcshareddata/swiftpm/Package.resolved | 6 +- .../AccessoryManager+ToRadio.swift | 2 +- .../Accessory Manager/AccessoryManager.swift | 2 + .../CoreData/ChannelEntityExtension.swift | 2 +- Meshtastic/Helpers/MeshPackets.swift | 2 +- MeshtasticProtobufs/Package.swift | 2 +- .../Sources/meshtastic/admin.pb.swift | 516 +----- .../Sources/meshtastic/apponly.pb.swift | 13 +- .../Sources/meshtastic/atak.pb.swift | 137 +- .../meshtastic/cannedmessages.pb.swift | 12 +- .../Sources/meshtastic/channel.pb.swift | 87 +- .../Sources/meshtastic/clientonly.pb.swift | 19 +- .../Sources/meshtastic/config.pb.swift | 809 +++------- .../meshtastic/connection_status.pb.swift | 58 +- .../Sources/meshtastic/device_ui.pb.swift | 186 +-- .../Sources/meshtastic/deviceonly.pb.swift | 107 +- .../Sources/meshtastic/interdevice.pb.swift | 90 +- .../Sources/meshtastic/localonly.pb.swift | 48 +- .../Sources/meshtastic/mesh.pb.swift | 1404 +++++------------ .../Sources/meshtastic/module_config.pb.swift | 533 ++----- .../Sources/meshtastic/mqtt.pb.swift | 34 +- .../Sources/meshtastic/paxcount.pb.swift | 14 +- .../Sources/meshtastic/portnums.pb.swift | 61 +- .../Sources/meshtastic/powermon.pb.swift | 161 +- .../meshtastic/remote_hardware.pb.swift | 52 +- .../Sources/meshtastic/rtttl.pb.swift | 12 +- .../Sources/meshtastic/storeforward.pb.swift | 144 +- .../Sources/meshtastic/telemetry.pb.swift | 296 +--- .../Sources/meshtastic/xmodem.pb.swift | 58 +- protobufs | 2 +- 30 files changed, 1205 insertions(+), 3664 deletions(-) diff --git a/Meshtastic.xcworkspace/xcshareddata/swiftpm/Package.resolved b/Meshtastic.xcworkspace/xcshareddata/swiftpm/Package.resolved index d0b85617..ba8776f7 100644 --- a/Meshtastic.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/Meshtastic.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -1,5 +1,5 @@ { - "originHash" : "0fb5b226f8ca0b357ce7816ebac09d017cbe0ad253452876e5e2ff8e555c7124", + "originHash" : "25240dd07109fa832be10093f5d97529f872f18e8d9df6468e5e4212bc0b487e", "pins" : [ { "identity" : "cocoamqtt", @@ -60,8 +60,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/apple/swift-protobuf.git", "state" : { - "revision" : "d72aed98f8253ec1aa9ea1141e28150f408cf17f", - "version" : "1.29.0" + "revision" : "c169a5744230951031770e27e475ff6eefe51f9d", + "version" : "1.33.3" } } ], diff --git a/Meshtastic/Accessory/Accessory Manager/AccessoryManager+ToRadio.swift b/Meshtastic/Accessory/Accessory Manager/AccessoryManager+ToRadio.swift index 7c928b3d..4fe2ffaf 100644 --- a/Meshtastic/Accessory/Accessory Manager/AccessoryManager+ToRadio.swift +++ b/Meshtastic/Accessory/Accessory Manager/AccessoryManager+ToRadio.swift @@ -1814,7 +1814,7 @@ extension AccessoryManager { public func sendNodeDBReset(fromUser: UserEntity, toUser: UserEntity) async throws { var adminPacket = AdminMessage() - adminPacket.nodedbReset = 5 + adminPacket.nodedbReset = true if fromUser != toUser { adminPacket.sessionPasskey = toUser.userNode?.sessionPasskey ?? Data() } diff --git a/Meshtastic/Accessory/Accessory Manager/AccessoryManager.swift b/Meshtastic/Accessory/Accessory Manager/AccessoryManager.swift index 2dd5fb35..07513866 100644 --- a/Meshtastic/Accessory/Accessory Manager/AccessoryManager.swift +++ b/Meshtastic/Accessory/Accessory Manager/AccessoryManager.swift @@ -578,6 +578,8 @@ class AccessoryManager: ObservableObject, MqttClientProxyManagerDelegate { Logger.mesh.info("🕸️ MESH PACKET received for ATAK Forwarder App UNHANDLED UNHANDLED") case .simulatorApp: Logger.mesh.info("🕸️ MESH PACKET received for Simulator App UNHANDLED UNHANDLED") + case .storeForwardPlusplusApp: + Logger.mesh.info("🕸️ MESH PACKET received for SFPP App UNHANDLED UNHANDLED") case .audioApp: Logger.mesh.info("🕸️ MESH PACKET received for Audio App UNHANDLED UNHANDLED") case .tracerouteApp: diff --git a/Meshtastic/Extensions/CoreData/ChannelEntityExtension.swift b/Meshtastic/Extensions/CoreData/ChannelEntityExtension.swift index 5916567c..8fce761c 100644 --- a/Meshtastic/Extensions/CoreData/ChannelEntityExtension.swift +++ b/Meshtastic/Extensions/CoreData/ChannelEntityExtension.swift @@ -55,7 +55,7 @@ extension ChannelEntity { channel.settings.psk = self.psk ?? Data() channel.role = Channel.Role(rawValue: Int(self.role)) ?? Channel.Role.secondary channel.settings.moduleSettings.positionPrecision = UInt32(self.positionPrecision) - channel.settings.moduleSettings.isClientMuted = self.mute + channel.settings.moduleSettings.isMuted = self.mute return channel } } diff --git a/Meshtastic/Helpers/MeshPackets.swift b/Meshtastic/Helpers/MeshPackets.swift index 1e68896b..255417c4 100644 --- a/Meshtastic/Helpers/MeshPackets.swift +++ b/Meshtastic/Helpers/MeshPackets.swift @@ -181,7 +181,7 @@ func channelPacket (channel: Channel, fromNum: Int64, context: NSManagedObjectCo newChannel.psk = channel.settings.psk if channel.settings.hasModuleSettings { newChannel.positionPrecision = Int32(truncatingIfNeeded: channel.settings.moduleSettings.positionPrecision) - newChannel.mute = channel.settings.moduleSettings.isClientMuted + newChannel.mute = channel.settings.moduleSettings.isMuted } guard let mutableChannels = fetchedMyInfo[0].channels!.mutableCopy() as? NSMutableOrderedSet else { return diff --git a/MeshtasticProtobufs/Package.swift b/MeshtasticProtobufs/Package.swift index d329c439..f8715190 100644 --- a/MeshtasticProtobufs/Package.swift +++ b/MeshtasticProtobufs/Package.swift @@ -11,7 +11,7 @@ let package = Package( ), ], dependencies: [ - .package(url: "https://github.com/apple/swift-protobuf.git", from: "1.19.0"), + .package(url: "https://github.com/apple/swift-protobuf.git", from: "1.33.3"), ], targets: [ .target( diff --git a/MeshtasticProtobufs/Sources/meshtastic/admin.pb.swift b/MeshtasticProtobufs/Sources/meshtastic/admin.pb.swift index 138d2ed1..fa6a9e61 100644 --- a/MeshtasticProtobufs/Sources/meshtastic/admin.pb.swift +++ b/MeshtasticProtobufs/Sources/meshtastic/admin.pb.swift @@ -1,5 +1,6 @@ // DO NOT EDIT. // swift-format-ignore-file +// swiftlint:disable all // // Generated by the Swift generator plugin for the protocol buffer compiler. // Source: meshtastic/admin.proto @@ -24,7 +25,7 @@ fileprivate struct _GeneratedWithProtocGenSwiftVersion: SwiftProtobuf.ProtobufAP /// 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. /// (Prior to 1.2 these operations were done via special ToRadio operations) -public struct AdminMessage { +public struct AdminMessage: 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. @@ -582,10 +583,11 @@ public struct AdminMessage { /// /// Tell the node to reset the nodedb. - public var nodedbReset: Int32 { + /// When true, favorites are preserved through reset. + public var nodedbReset: Bool { get { if case .nodedbReset(let v)? = payloadVariant {return v} - return 0 + return false } set {payloadVariant = .nodedbReset(newValue)} } @@ -594,7 +596,7 @@ public struct AdminMessage { /// /// TODO: REPLACE - public enum OneOf_PayloadVariant: Equatable { + public enum OneOf_PayloadVariant: Equatable, Sendable { /// /// Send the specified channel in the response to this message /// NOTE: This field is sent with the channel index + 1 (to ensure we never try to send 'zero' - which protobufs treats as not present) @@ -767,239 +769,14 @@ public struct AdminMessage { case factoryResetConfig(Int32) /// /// Tell the node to reset the nodedb. - case nodedbReset(Int32) + /// When true, favorites are preserved through reset. + case nodedbReset(Bool) - #if !swift(>=4.1) - public static func ==(lhs: AdminMessage.OneOf_PayloadVariant, rhs: AdminMessage.OneOf_PayloadVariant) -> Bool { - // 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 (lhs, rhs) { - case (.getChannelRequest, .getChannelRequest): return { - guard case .getChannelRequest(let l) = lhs, case .getChannelRequest(let r) = rhs else { preconditionFailure() } - return l == r - }() - case (.getChannelResponse, .getChannelResponse): return { - guard case .getChannelResponse(let l) = lhs, case .getChannelResponse(let r) = rhs else { preconditionFailure() } - return l == r - }() - case (.getOwnerRequest, .getOwnerRequest): return { - guard case .getOwnerRequest(let l) = lhs, case .getOwnerRequest(let r) = rhs else { preconditionFailure() } - return l == r - }() - case (.getOwnerResponse, .getOwnerResponse): return { - guard case .getOwnerResponse(let l) = lhs, case .getOwnerResponse(let r) = rhs else { preconditionFailure() } - return l == r - }() - case (.getConfigRequest, .getConfigRequest): return { - guard case .getConfigRequest(let l) = lhs, case .getConfigRequest(let r) = rhs else { preconditionFailure() } - return l == r - }() - case (.getConfigResponse, .getConfigResponse): return { - guard case .getConfigResponse(let l) = lhs, case .getConfigResponse(let r) = rhs else { preconditionFailure() } - return l == r - }() - case (.getModuleConfigRequest, .getModuleConfigRequest): return { - guard case .getModuleConfigRequest(let l) = lhs, case .getModuleConfigRequest(let r) = rhs else { preconditionFailure() } - return l == r - }() - case (.getModuleConfigResponse, .getModuleConfigResponse): return { - guard case .getModuleConfigResponse(let l) = lhs, case .getModuleConfigResponse(let r) = rhs else { preconditionFailure() } - return l == r - }() - case (.getCannedMessageModuleMessagesRequest, .getCannedMessageModuleMessagesRequest): return { - guard case .getCannedMessageModuleMessagesRequest(let l) = lhs, case .getCannedMessageModuleMessagesRequest(let r) = rhs else { preconditionFailure() } - return l == r - }() - case (.getCannedMessageModuleMessagesResponse, .getCannedMessageModuleMessagesResponse): return { - guard case .getCannedMessageModuleMessagesResponse(let l) = lhs, case .getCannedMessageModuleMessagesResponse(let r) = rhs else { preconditionFailure() } - return l == r - }() - case (.getDeviceMetadataRequest, .getDeviceMetadataRequest): return { - guard case .getDeviceMetadataRequest(let l) = lhs, case .getDeviceMetadataRequest(let r) = rhs else { preconditionFailure() } - return l == r - }() - case (.getDeviceMetadataResponse, .getDeviceMetadataResponse): return { - guard case .getDeviceMetadataResponse(let l) = lhs, case .getDeviceMetadataResponse(let r) = rhs else { preconditionFailure() } - return l == r - }() - case (.getRingtoneRequest, .getRingtoneRequest): return { - guard case .getRingtoneRequest(let l) = lhs, case .getRingtoneRequest(let r) = rhs else { preconditionFailure() } - return l == r - }() - case (.getRingtoneResponse, .getRingtoneResponse): return { - guard case .getRingtoneResponse(let l) = lhs, case .getRingtoneResponse(let r) = rhs else { preconditionFailure() } - return l == r - }() - case (.getDeviceConnectionStatusRequest, .getDeviceConnectionStatusRequest): return { - guard case .getDeviceConnectionStatusRequest(let l) = lhs, case .getDeviceConnectionStatusRequest(let r) = rhs else { preconditionFailure() } - return l == r - }() - case (.getDeviceConnectionStatusResponse, .getDeviceConnectionStatusResponse): return { - guard case .getDeviceConnectionStatusResponse(let l) = lhs, case .getDeviceConnectionStatusResponse(let r) = rhs else { preconditionFailure() } - return l == r - }() - case (.setHamMode, .setHamMode): return { - guard case .setHamMode(let l) = lhs, case .setHamMode(let r) = rhs else { preconditionFailure() } - return l == r - }() - case (.getNodeRemoteHardwarePinsRequest, .getNodeRemoteHardwarePinsRequest): return { - guard case .getNodeRemoteHardwarePinsRequest(let l) = lhs, case .getNodeRemoteHardwarePinsRequest(let r) = rhs else { preconditionFailure() } - return l == r - }() - case (.getNodeRemoteHardwarePinsResponse, .getNodeRemoteHardwarePinsResponse): return { - guard case .getNodeRemoteHardwarePinsResponse(let l) = lhs, case .getNodeRemoteHardwarePinsResponse(let r) = rhs else { preconditionFailure() } - return l == r - }() - case (.enterDfuModeRequest, .enterDfuModeRequest): return { - guard case .enterDfuModeRequest(let l) = lhs, case .enterDfuModeRequest(let r) = rhs else { preconditionFailure() } - return l == r - }() - case (.deleteFileRequest, .deleteFileRequest): return { - guard case .deleteFileRequest(let l) = lhs, case .deleteFileRequest(let r) = rhs else { preconditionFailure() } - return l == r - }() - case (.setScale, .setScale): return { - guard case .setScale(let l) = lhs, case .setScale(let r) = rhs else { preconditionFailure() } - return l == r - }() - case (.backupPreferences, .backupPreferences): return { - guard case .backupPreferences(let l) = lhs, case .backupPreferences(let r) = rhs else { preconditionFailure() } - return l == r - }() - case (.restorePreferences, .restorePreferences): return { - guard case .restorePreferences(let l) = lhs, case .restorePreferences(let r) = rhs else { preconditionFailure() } - return l == r - }() - case (.removeBackupPreferences, .removeBackupPreferences): return { - guard case .removeBackupPreferences(let l) = lhs, case .removeBackupPreferences(let r) = rhs else { preconditionFailure() } - return l == r - }() - case (.sendInputEvent, .sendInputEvent): return { - guard case .sendInputEvent(let l) = lhs, case .sendInputEvent(let r) = rhs else { preconditionFailure() } - return l == r - }() - case (.setOwner, .setOwner): return { - guard case .setOwner(let l) = lhs, case .setOwner(let r) = rhs else { preconditionFailure() } - return l == r - }() - case (.setChannel, .setChannel): return { - guard case .setChannel(let l) = lhs, case .setChannel(let r) = rhs else { preconditionFailure() } - return l == r - }() - case (.setConfig, .setConfig): return { - guard case .setConfig(let l) = lhs, case .setConfig(let r) = rhs else { preconditionFailure() } - return l == r - }() - case (.setModuleConfig, .setModuleConfig): return { - guard case .setModuleConfig(let l) = lhs, case .setModuleConfig(let r) = rhs else { preconditionFailure() } - return l == r - }() - case (.setCannedMessageModuleMessages, .setCannedMessageModuleMessages): return { - guard case .setCannedMessageModuleMessages(let l) = lhs, case .setCannedMessageModuleMessages(let r) = rhs else { preconditionFailure() } - return l == r - }() - case (.setRingtoneMessage, .setRingtoneMessage): return { - guard case .setRingtoneMessage(let l) = lhs, case .setRingtoneMessage(let r) = rhs else { preconditionFailure() } - return l == r - }() - case (.removeByNodenum, .removeByNodenum): return { - guard case .removeByNodenum(let l) = lhs, case .removeByNodenum(let r) = rhs else { preconditionFailure() } - return l == r - }() - case (.setFavoriteNode, .setFavoriteNode): return { - guard case .setFavoriteNode(let l) = lhs, case .setFavoriteNode(let r) = rhs else { preconditionFailure() } - return l == r - }() - case (.removeFavoriteNode, .removeFavoriteNode): return { - guard case .removeFavoriteNode(let l) = lhs, case .removeFavoriteNode(let r) = rhs else { preconditionFailure() } - return l == r - }() - case (.setFixedPosition, .setFixedPosition): return { - guard case .setFixedPosition(let l) = lhs, case .setFixedPosition(let r) = rhs else { preconditionFailure() } - return l == r - }() - case (.removeFixedPosition, .removeFixedPosition): return { - guard case .removeFixedPosition(let l) = lhs, case .removeFixedPosition(let r) = rhs else { preconditionFailure() } - return l == r - }() - case (.setTimeOnly, .setTimeOnly): return { - guard case .setTimeOnly(let l) = lhs, case .setTimeOnly(let r) = rhs else { preconditionFailure() } - return l == r - }() - case (.getUiConfigRequest, .getUiConfigRequest): return { - guard case .getUiConfigRequest(let l) = lhs, case .getUiConfigRequest(let r) = rhs else { preconditionFailure() } - return l == r - }() - case (.getUiConfigResponse, .getUiConfigResponse): return { - guard case .getUiConfigResponse(let l) = lhs, case .getUiConfigResponse(let r) = rhs else { preconditionFailure() } - return l == r - }() - case (.storeUiConfig, .storeUiConfig): return { - guard case .storeUiConfig(let l) = lhs, case .storeUiConfig(let r) = rhs else { preconditionFailure() } - return l == r - }() - case (.setIgnoredNode, .setIgnoredNode): return { - guard case .setIgnoredNode(let l) = lhs, case .setIgnoredNode(let r) = rhs else { preconditionFailure() } - return l == r - }() - case (.removeIgnoredNode, .removeIgnoredNode): return { - guard case .removeIgnoredNode(let l) = lhs, case .removeIgnoredNode(let r) = rhs else { preconditionFailure() } - return l == r - }() - case (.beginEditSettings, .beginEditSettings): return { - guard case .beginEditSettings(let l) = lhs, case .beginEditSettings(let r) = rhs else { preconditionFailure() } - return l == r - }() - case (.commitEditSettings, .commitEditSettings): return { - guard case .commitEditSettings(let l) = lhs, case .commitEditSettings(let r) = rhs else { preconditionFailure() } - return l == r - }() - case (.addContact, .addContact): return { - guard case .addContact(let l) = lhs, case .addContact(let r) = rhs else { preconditionFailure() } - return l == r - }() - case (.keyVerification, .keyVerification): return { - guard case .keyVerification(let l) = lhs, case .keyVerification(let r) = rhs else { preconditionFailure() } - return l == r - }() - case (.factoryResetDevice, .factoryResetDevice): return { - guard case .factoryResetDevice(let l) = lhs, case .factoryResetDevice(let r) = rhs else { preconditionFailure() } - return l == r - }() - case (.rebootOtaSeconds, .rebootOtaSeconds): return { - guard case .rebootOtaSeconds(let l) = lhs, case .rebootOtaSeconds(let r) = rhs else { preconditionFailure() } - return l == r - }() - case (.exitSimulator, .exitSimulator): return { - guard case .exitSimulator(let l) = lhs, case .exitSimulator(let r) = rhs else { preconditionFailure() } - return l == r - }() - case (.rebootSeconds, .rebootSeconds): return { - guard case .rebootSeconds(let l) = lhs, case .rebootSeconds(let r) = rhs else { preconditionFailure() } - return l == r - }() - case (.shutdownSeconds, .shutdownSeconds): return { - guard case .shutdownSeconds(let l) = lhs, case .shutdownSeconds(let r) = rhs else { preconditionFailure() } - return l == r - }() - case (.factoryResetConfig, .factoryResetConfig): return { - guard case .factoryResetConfig(let l) = lhs, case .factoryResetConfig(let r) = rhs else { preconditionFailure() } - return l == r - }() - case (.nodedbReset, .nodedbReset): return { - guard case .nodedbReset(let l) = lhs, case .nodedbReset(let r) = rhs else { preconditionFailure() } - return l == r - }() - default: return false - } - } - #endif } /// /// TODO: REPLACE - public enum ConfigType: SwiftProtobuf.Enum { + public enum ConfigType: SwiftProtobuf.Enum, Swift.CaseIterable { public typealias RawValue = Int /// @@ -1079,11 +856,25 @@ public struct AdminMessage { } } + // The compiler won't synthesize support with the UNRECOGNIZED case. + public static let allCases: [AdminMessage.ConfigType] = [ + .deviceConfig, + .positionConfig, + .powerConfig, + .networkConfig, + .displayConfig, + .loraConfig, + .bluetoothConfig, + .securityConfig, + .sessionkeyConfig, + .deviceuiConfig, + ] + } /// /// TODO: REPLACE - public enum ModuleConfigType: SwiftProtobuf.Enum { + public enum ModuleConfigType: SwiftProtobuf.Enum, Swift.CaseIterable { public typealias RawValue = Int /// @@ -1181,9 +972,26 @@ public struct AdminMessage { } } + // The compiler won't synthesize support with the UNRECOGNIZED case. + public static let allCases: [AdminMessage.ModuleConfigType] = [ + .mqttConfig, + .serialConfig, + .extnotifConfig, + .storeforwardConfig, + .rangetestConfig, + .telemetryConfig, + .cannedmsgConfig, + .audioConfig, + .remotehardwareConfig, + .neighborinfoConfig, + .ambientlightingConfig, + .detectionsensorConfig, + .paxcounterConfig, + ] + } - public enum BackupLocation: SwiftProtobuf.Enum { + public enum BackupLocation: SwiftProtobuf.Enum, Swift.CaseIterable { public typealias RawValue = Int /// @@ -1215,11 +1023,17 @@ public struct AdminMessage { } } + // The compiler won't synthesize support with the UNRECOGNIZED case. + public static let allCases: [AdminMessage.BackupLocation] = [ + .flash, + .sd, + ] + } /// /// Input event message to be sent to the node. - public struct InputEvent { + public struct InputEvent: 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. @@ -1248,56 +1062,9 @@ public struct AdminMessage { public init() {} } -#if swift(>=4.2) - -extension AdminMessage.ConfigType: CaseIterable { - // The compiler won't synthesize support with the UNRECOGNIZED case. - public static let allCases: [AdminMessage.ConfigType] = [ - .deviceConfig, - .positionConfig, - .powerConfig, - .networkConfig, - .displayConfig, - .loraConfig, - .bluetoothConfig, - .securityConfig, - .sessionkeyConfig, - .deviceuiConfig, - ] -} - -extension AdminMessage.ModuleConfigType: CaseIterable { - // The compiler won't synthesize support with the UNRECOGNIZED case. - public static let allCases: [AdminMessage.ModuleConfigType] = [ - .mqttConfig, - .serialConfig, - .extnotifConfig, - .storeforwardConfig, - .rangetestConfig, - .telemetryConfig, - .cannedmsgConfig, - .audioConfig, - .remotehardwareConfig, - .neighborinfoConfig, - .ambientlightingConfig, - .detectionsensorConfig, - .paxcounterConfig, - ] -} - -extension AdminMessage.BackupLocation: CaseIterable { - // The compiler won't synthesize support with the UNRECOGNIZED case. - public static let allCases: [AdminMessage.BackupLocation] = [ - .flash, - .sd, - ] -} - -#endif // swift(>=4.2) - /// /// Parameters for setting up Meshtastic for ameteur radio usage -public struct HamParameters { +public struct HamParameters: 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. @@ -1327,7 +1094,7 @@ public struct HamParameters { /// /// Response envelope for node_remote_hardware_pins -public struct NodeRemoteHardwarePinsResponse { +public struct NodeRemoteHardwarePinsResponse: 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. @@ -1341,7 +1108,7 @@ public struct NodeRemoteHardwarePinsResponse { public init() {} } -public struct SharedContact { +public struct SharedContact: 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. @@ -1378,7 +1145,7 @@ public struct SharedContact { /// /// This message is used by a client to initiate or complete a key verification -public struct KeyVerificationAdmin { +public struct KeyVerificationAdmin: 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. @@ -1408,7 +1175,7 @@ public struct KeyVerificationAdmin { /// /// Three stages of this request. - public enum MessageType: SwiftProtobuf.Enum { + public enum MessageType: SwiftProtobuf.Enum, Swift.CaseIterable { public typealias RawValue = Int /// @@ -1453,6 +1220,14 @@ public struct KeyVerificationAdmin { } } + // The compiler won't synthesize support with the UNRECOGNIZED case. + public static let allCases: [KeyVerificationAdmin.MessageType] = [ + .initiateVerification, + .provideSecurityNumber, + .doVerify, + .doNotVerify, + ] + } public init() {} @@ -1460,97 +1235,13 @@ public struct KeyVerificationAdmin { fileprivate var _securityNumber: UInt32? = nil } -#if swift(>=4.2) - -extension KeyVerificationAdmin.MessageType: CaseIterable { - // The compiler won't synthesize support with the UNRECOGNIZED case. - public static let allCases: [KeyVerificationAdmin.MessageType] = [ - .initiateVerification, - .provideSecurityNumber, - .doVerify, - .doNotVerify, - ] -} - -#endif // swift(>=4.2) - -#if swift(>=5.5) && canImport(_Concurrency) -extension AdminMessage: @unchecked Sendable {} -extension AdminMessage.OneOf_PayloadVariant: @unchecked Sendable {} -extension AdminMessage.ConfigType: @unchecked Sendable {} -extension AdminMessage.ModuleConfigType: @unchecked Sendable {} -extension AdminMessage.BackupLocation: @unchecked Sendable {} -extension AdminMessage.InputEvent: @unchecked Sendable {} -extension HamParameters: @unchecked Sendable {} -extension NodeRemoteHardwarePinsResponse: @unchecked Sendable {} -extension SharedContact: @unchecked Sendable {} -extension KeyVerificationAdmin: @unchecked Sendable {} -extension KeyVerificationAdmin.MessageType: @unchecked Sendable {} -#endif // swift(>=5.5) && canImport(_Concurrency) - // MARK: - Code below here is support for the SwiftProtobuf runtime. fileprivate let _protobuf_package = "meshtastic" extension AdminMessage: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { public static let protoMessageName: String = _protobuf_package + ".AdminMessage" - public static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ - 101: .standard(proto: "session_passkey"), - 1: .standard(proto: "get_channel_request"), - 2: .standard(proto: "get_channel_response"), - 3: .standard(proto: "get_owner_request"), - 4: .standard(proto: "get_owner_response"), - 5: .standard(proto: "get_config_request"), - 6: .standard(proto: "get_config_response"), - 7: .standard(proto: "get_module_config_request"), - 8: .standard(proto: "get_module_config_response"), - 10: .standard(proto: "get_canned_message_module_messages_request"), - 11: .standard(proto: "get_canned_message_module_messages_response"), - 12: .standard(proto: "get_device_metadata_request"), - 13: .standard(proto: "get_device_metadata_response"), - 14: .standard(proto: "get_ringtone_request"), - 15: .standard(proto: "get_ringtone_response"), - 16: .standard(proto: "get_device_connection_status_request"), - 17: .standard(proto: "get_device_connection_status_response"), - 18: .standard(proto: "set_ham_mode"), - 19: .standard(proto: "get_node_remote_hardware_pins_request"), - 20: .standard(proto: "get_node_remote_hardware_pins_response"), - 21: .standard(proto: "enter_dfu_mode_request"), - 22: .standard(proto: "delete_file_request"), - 23: .standard(proto: "set_scale"), - 24: .standard(proto: "backup_preferences"), - 25: .standard(proto: "restore_preferences"), - 26: .standard(proto: "remove_backup_preferences"), - 27: .standard(proto: "send_input_event"), - 32: .standard(proto: "set_owner"), - 33: .standard(proto: "set_channel"), - 34: .standard(proto: "set_config"), - 35: .standard(proto: "set_module_config"), - 36: .standard(proto: "set_canned_message_module_messages"), - 37: .standard(proto: "set_ringtone_message"), - 38: .standard(proto: "remove_by_nodenum"), - 39: .standard(proto: "set_favorite_node"), - 40: .standard(proto: "remove_favorite_node"), - 41: .standard(proto: "set_fixed_position"), - 42: .standard(proto: "remove_fixed_position"), - 43: .standard(proto: "set_time_only"), - 44: .standard(proto: "get_ui_config_request"), - 45: .standard(proto: "get_ui_config_response"), - 46: .standard(proto: "store_ui_config"), - 47: .standard(proto: "set_ignored_node"), - 48: .standard(proto: "remove_ignored_node"), - 64: .standard(proto: "begin_edit_settings"), - 65: .standard(proto: "commit_edit_settings"), - 66: .standard(proto: "add_contact"), - 67: .standard(proto: "key_verification"), - 94: .standard(proto: "factory_reset_device"), - 95: .standard(proto: "reboot_ota_seconds"), - 96: .standard(proto: "exit_simulator"), - 97: .standard(proto: "reboot_seconds"), - 98: .standard(proto: "shutdown_seconds"), - 99: .standard(proto: "factory_reset_config"), - 100: .standard(proto: "nodedb_reset"), - ] + 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() { @@ -2073,8 +1764,8 @@ extension AdminMessage: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementat } }() case 100: try { - var v: Int32? - try decoder.decodeSingularInt32Field(value: &v) + var v: Bool? + try decoder.decodeSingularBoolField(value: &v) if let v = v { if self.payloadVariant != nil {try decoder.handleConflictingOneOf()} self.payloadVariant = .nodedbReset(v) @@ -2306,7 +1997,7 @@ extension AdminMessage: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementat }() case .nodedbReset?: try { guard case .nodedbReset(let v)? = self.payloadVariant else { preconditionFailure() } - try visitor.visitSingularInt32Field(value: v, fieldNumber: 100) + try visitor.visitSingularBoolField(value: v, fieldNumber: 100) }() case nil: break } @@ -2325,53 +2016,20 @@ extension AdminMessage: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementat } extension AdminMessage.ConfigType: SwiftProtobuf._ProtoNameProviding { - public static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ - 0: .same(proto: "DEVICE_CONFIG"), - 1: .same(proto: "POSITION_CONFIG"), - 2: .same(proto: "POWER_CONFIG"), - 3: .same(proto: "NETWORK_CONFIG"), - 4: .same(proto: "DISPLAY_CONFIG"), - 5: .same(proto: "LORA_CONFIG"), - 6: .same(proto: "BLUETOOTH_CONFIG"), - 7: .same(proto: "SECURITY_CONFIG"), - 8: .same(proto: "SESSIONKEY_CONFIG"), - 9: .same(proto: "DEVICEUI_CONFIG"), - ] + public static let _protobuf_nameMap = SwiftProtobuf._NameMap(bytecode: "\0\u{2}\0DEVICE_CONFIG\0\u{1}POSITION_CONFIG\0\u{1}POWER_CONFIG\0\u{1}NETWORK_CONFIG\0\u{1}DISPLAY_CONFIG\0\u{1}LORA_CONFIG\0\u{1}BLUETOOTH_CONFIG\0\u{1}SECURITY_CONFIG\0\u{1}SESSIONKEY_CONFIG\0\u{1}DEVICEUI_CONFIG\0") } extension AdminMessage.ModuleConfigType: SwiftProtobuf._ProtoNameProviding { - public static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ - 0: .same(proto: "MQTT_CONFIG"), - 1: .same(proto: "SERIAL_CONFIG"), - 2: .same(proto: "EXTNOTIF_CONFIG"), - 3: .same(proto: "STOREFORWARD_CONFIG"), - 4: .same(proto: "RANGETEST_CONFIG"), - 5: .same(proto: "TELEMETRY_CONFIG"), - 6: .same(proto: "CANNEDMSG_CONFIG"), - 7: .same(proto: "AUDIO_CONFIG"), - 8: .same(proto: "REMOTEHARDWARE_CONFIG"), - 9: .same(proto: "NEIGHBORINFO_CONFIG"), - 10: .same(proto: "AMBIENTLIGHTING_CONFIG"), - 11: .same(proto: "DETECTIONSENSOR_CONFIG"), - 12: .same(proto: "PAXCOUNTER_CONFIG"), - ] + public static let _protobuf_nameMap = SwiftProtobuf._NameMap(bytecode: "\0\u{2}\0MQTT_CONFIG\0\u{1}SERIAL_CONFIG\0\u{1}EXTNOTIF_CONFIG\0\u{1}STOREFORWARD_CONFIG\0\u{1}RANGETEST_CONFIG\0\u{1}TELEMETRY_CONFIG\0\u{1}CANNEDMSG_CONFIG\0\u{1}AUDIO_CONFIG\0\u{1}REMOTEHARDWARE_CONFIG\0\u{1}NEIGHBORINFO_CONFIG\0\u{1}AMBIENTLIGHTING_CONFIG\0\u{1}DETECTIONSENSOR_CONFIG\0\u{1}PAXCOUNTER_CONFIG\0") } extension AdminMessage.BackupLocation: SwiftProtobuf._ProtoNameProviding { - public static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ - 0: .same(proto: "FLASH"), - 1: .same(proto: "SD"), - ] + public static let _protobuf_nameMap = SwiftProtobuf._NameMap(bytecode: "\0\u{2}\0FLASH\0\u{1}SD\0") } extension AdminMessage.InputEvent: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { public static let protoMessageName: String = AdminMessage.protoMessageName + ".InputEvent" - public static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ - 1: .standard(proto: "event_code"), - 2: .standard(proto: "kb_char"), - 3: .standard(proto: "touch_x"), - 4: .standard(proto: "touch_y"), - ] + public static let _protobuf_nameMap = SwiftProtobuf._NameMap(bytecode: "\0\u{3}event_code\0\u{3}kb_char\0\u{3}touch_x\0\u{3}touch_y\0") public mutating func decodeMessage(decoder: inout D) throws { while let fieldNumber = try decoder.nextFieldNumber() { @@ -2416,12 +2074,7 @@ extension AdminMessage.InputEvent: SwiftProtobuf.Message, SwiftProtobuf._Message extension HamParameters: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { public static let protoMessageName: String = _protobuf_package + ".HamParameters" - public static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ - 1: .standard(proto: "call_sign"), - 2: .standard(proto: "tx_power"), - 3: .same(proto: "frequency"), - 4: .standard(proto: "short_name"), - ] + 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") public mutating func decodeMessage(decoder: inout D) throws { while let fieldNumber = try decoder.nextFieldNumber() { @@ -2445,7 +2098,7 @@ extension HamParameters: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementa if self.txPower != 0 { try visitor.visitSingularInt32Field(value: self.txPower, fieldNumber: 2) } - if self.frequency != 0 { + if self.frequency.bitPattern != 0 { try visitor.visitSingularFloatField(value: self.frequency, fieldNumber: 3) } if !self.shortName.isEmpty { @@ -2466,9 +2119,7 @@ extension HamParameters: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementa extension NodeRemoteHardwarePinsResponse: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { public static let protoMessageName: String = _protobuf_package + ".NodeRemoteHardwarePinsResponse" - public static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ - 1: .standard(proto: "node_remote_hardware_pins"), - ] + public static let _protobuf_nameMap = SwiftProtobuf._NameMap(bytecode: "\0\u{3}node_remote_hardware_pins\0") public mutating func decodeMessage(decoder: inout D) throws { while let fieldNumber = try decoder.nextFieldNumber() { @@ -2498,12 +2149,7 @@ extension NodeRemoteHardwarePinsResponse: SwiftProtobuf.Message, SwiftProtobuf._ extension SharedContact: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { public static let protoMessageName: String = _protobuf_package + ".SharedContact" - public static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ - 1: .standard(proto: "node_num"), - 2: .same(proto: "user"), - 3: .standard(proto: "should_ignore"), - 4: .standard(proto: "manually_verified"), - ] + public static let _protobuf_nameMap = SwiftProtobuf._NameMap(bytecode: "\0\u{3}node_num\0\u{1}user\0\u{3}should_ignore\0\u{3}manually_verified\0") public mutating func decodeMessage(decoder: inout D) throws { while let fieldNumber = try decoder.nextFieldNumber() { @@ -2552,12 +2198,7 @@ extension SharedContact: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementa extension KeyVerificationAdmin: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { public static let protoMessageName: String = _protobuf_package + ".KeyVerificationAdmin" - public static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ - 1: .standard(proto: "message_type"), - 2: .standard(proto: "remote_nodenum"), - 3: .same(proto: "nonce"), - 4: .standard(proto: "security_number"), - ] + public static let _protobuf_nameMap = SwiftProtobuf._NameMap(bytecode: "\0\u{3}message_type\0\u{3}remote_nodenum\0\u{1}nonce\0\u{3}security_number\0") public mutating func decodeMessage(decoder: inout D) throws { while let fieldNumber = try decoder.nextFieldNumber() { @@ -2605,10 +2246,5 @@ extension KeyVerificationAdmin: SwiftProtobuf.Message, SwiftProtobuf._MessageImp } extension KeyVerificationAdmin.MessageType: SwiftProtobuf._ProtoNameProviding { - public static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ - 0: .same(proto: "INITIATE_VERIFICATION"), - 1: .same(proto: "PROVIDE_SECURITY_NUMBER"), - 2: .same(proto: "DO_VERIFY"), - 3: .same(proto: "DO_NOT_VERIFY"), - ] + public static let _protobuf_nameMap = SwiftProtobuf._NameMap(bytecode: "\0\u{2}\0INITIATE_VERIFICATION\0\u{1}PROVIDE_SECURITY_NUMBER\0\u{1}DO_VERIFY\0\u{1}DO_NOT_VERIFY\0") } diff --git a/MeshtasticProtobufs/Sources/meshtastic/apponly.pb.swift b/MeshtasticProtobufs/Sources/meshtastic/apponly.pb.swift index 0457077c..9ade8540 100644 --- a/MeshtasticProtobufs/Sources/meshtastic/apponly.pb.swift +++ b/MeshtasticProtobufs/Sources/meshtastic/apponly.pb.swift @@ -1,5 +1,6 @@ // DO NOT EDIT. // swift-format-ignore-file +// swiftlint:disable all // // Generated by the Swift generator plugin for the protocol buffer compiler. // Source: meshtastic/apponly.proto @@ -7,7 +8,6 @@ // For information on using the generated types, please see the documentation: // https://github.com/apple/swift-protobuf/ -import Foundation import SwiftProtobuf // If the compiler emits an error on this type, it is because this file @@ -26,7 +26,7 @@ fileprivate struct _GeneratedWithProtocGenSwiftVersion: SwiftProtobuf.ProtobufAP /// any SECONDARY channels. /// No DISABLED channels are included. /// This abstraction is used only on the the 'app side' of the world (ie python, javascript and android etc) to show a group of Channels as a (long) URL -public struct ChannelSet { +public struct ChannelSet: 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. @@ -53,20 +53,13 @@ public struct ChannelSet { fileprivate var _loraConfig: Config.LoRaConfig? = nil } -#if swift(>=5.5) && canImport(_Concurrency) -extension ChannelSet: @unchecked Sendable {} -#endif // swift(>=5.5) && canImport(_Concurrency) - // MARK: - Code below here is support for the SwiftProtobuf runtime. fileprivate let _protobuf_package = "meshtastic" extension ChannelSet: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { public static let protoMessageName: String = _protobuf_package + ".ChannelSet" - public static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ - 1: .same(proto: "settings"), - 2: .standard(proto: "lora_config"), - ] + public static let _protobuf_nameMap = SwiftProtobuf._NameMap(bytecode: "\0\u{1}settings\0\u{3}lora_config\0") public mutating func decodeMessage(decoder: inout D) throws { while let fieldNumber = try decoder.nextFieldNumber() { diff --git a/MeshtasticProtobufs/Sources/meshtastic/atak.pb.swift b/MeshtasticProtobufs/Sources/meshtastic/atak.pb.swift index 867648a9..3076a6da 100644 --- a/MeshtasticProtobufs/Sources/meshtastic/atak.pb.swift +++ b/MeshtasticProtobufs/Sources/meshtastic/atak.pb.swift @@ -1,5 +1,6 @@ // DO NOT EDIT. // swift-format-ignore-file +// swiftlint:disable all // // Generated by the Swift generator plugin for the protocol buffer compiler. // Source: meshtastic/atak.proto @@ -20,7 +21,7 @@ fileprivate struct _GeneratedWithProtocGenSwiftVersion: SwiftProtobuf.ProtobufAP typealias Version = _2 } -public enum Team: SwiftProtobuf.Enum { +public enum Team: SwiftProtobuf.Enum, Swift.CaseIterable { public typealias RawValue = Int /// @@ -130,11 +131,6 @@ public enum Team: SwiftProtobuf.Enum { } } -} - -#if swift(>=4.2) - -extension Team: CaseIterable { // The compiler won't synthesize support with the UNRECOGNIZED case. public static let allCases: [Team] = [ .unspecifedColor, @@ -153,13 +149,12 @@ extension Team: CaseIterable { .darkGreen, .brown, ] -} -#endif // swift(>=4.2) +} /// /// Role of the group member -public enum MemberRole: SwiftProtobuf.Enum { +public enum MemberRole: SwiftProtobuf.Enum, Swift.CaseIterable { public typealias RawValue = Int /// @@ -233,11 +228,6 @@ public enum MemberRole: SwiftProtobuf.Enum { } } -} - -#if swift(>=4.2) - -extension MemberRole: CaseIterable { // The compiler won't synthesize support with the UNRECOGNIZED case. public static let allCases: [MemberRole] = [ .unspecifed, @@ -250,13 +240,12 @@ extension MemberRole: CaseIterable { .rto, .k9, ] -} -#endif // swift(>=4.2) +} /// /// Packets for the official ATAK Plugin -public struct TAKPacket { +public struct TAKPacket: 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. @@ -337,7 +326,7 @@ public struct TAKPacket { /// /// The payload of the packet - public enum OneOf_PayloadVariant: Equatable { + public enum OneOf_PayloadVariant: Equatable, Sendable { /// /// TAK position report case pli(PLI) @@ -349,28 +338,6 @@ public struct TAKPacket { /// May be compressed / truncated by the sender (EUD) case detail(Data) - #if !swift(>=4.1) - public static func ==(lhs: TAKPacket.OneOf_PayloadVariant, rhs: TAKPacket.OneOf_PayloadVariant) -> Bool { - // 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 (lhs, rhs) { - case (.pli, .pli): return { - guard case .pli(let l) = lhs, case .pli(let r) = rhs else { preconditionFailure() } - return l == r - }() - case (.chat, .chat): return { - guard case .chat(let l) = lhs, case .chat(let r) = rhs else { preconditionFailure() } - return l == r - }() - case (.detail, .detail): return { - guard case .detail(let l) = lhs, case .detail(let r) = rhs else { preconditionFailure() } - return l == r - }() - default: return false - } - } - #endif } public init() {} @@ -382,7 +349,7 @@ public struct TAKPacket { /// /// ATAK GeoChat message -public struct GeoChat { +public struct GeoChat: 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. @@ -424,7 +391,7 @@ public struct GeoChat { /// /// ATAK Group /// <__group role='Team Member' name='Cyan'/> -public struct Group { +public struct Group: 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. @@ -446,7 +413,7 @@ public struct Group { /// /// ATAK EUD Status /// -public struct Status { +public struct Status: 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. @@ -463,7 +430,7 @@ public struct Status { /// /// ATAK Contact /// -public struct Contact { +public struct Contact: 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. @@ -483,7 +450,7 @@ public struct Contact { /// /// Position Location Information from ATAK -public struct PLI { +public struct PLI: 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. @@ -515,67 +482,21 @@ public struct PLI { public init() {} } -#if swift(>=5.5) && canImport(_Concurrency) -extension Team: @unchecked Sendable {} -extension MemberRole: @unchecked Sendable {} -extension TAKPacket: @unchecked Sendable {} -extension TAKPacket.OneOf_PayloadVariant: @unchecked Sendable {} -extension GeoChat: @unchecked Sendable {} -extension Group: @unchecked Sendable {} -extension Status: @unchecked Sendable {} -extension Contact: @unchecked Sendable {} -extension PLI: @unchecked Sendable {} -#endif // swift(>=5.5) && canImport(_Concurrency) - // MARK: - Code below here is support for the SwiftProtobuf runtime. fileprivate let _protobuf_package = "meshtastic" extension Team: SwiftProtobuf._ProtoNameProviding { - public static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ - 0: .same(proto: "Unspecifed_Color"), - 1: .same(proto: "White"), - 2: .same(proto: "Yellow"), - 3: .same(proto: "Orange"), - 4: .same(proto: "Magenta"), - 5: .same(proto: "Red"), - 6: .same(proto: "Maroon"), - 7: .same(proto: "Purple"), - 8: .same(proto: "Dark_Blue"), - 9: .same(proto: "Blue"), - 10: .same(proto: "Cyan"), - 11: .same(proto: "Teal"), - 12: .same(proto: "Green"), - 13: .same(proto: "Dark_Green"), - 14: .same(proto: "Brown"), - ] + public static let _protobuf_nameMap = SwiftProtobuf._NameMap(bytecode: "\0\u{2}\0Unspecifed_Color\0\u{1}White\0\u{1}Yellow\0\u{1}Orange\0\u{1}Magenta\0\u{1}Red\0\u{1}Maroon\0\u{1}Purple\0\u{1}Dark_Blue\0\u{1}Blue\0\u{1}Cyan\0\u{1}Teal\0\u{1}Green\0\u{1}Dark_Green\0\u{1}Brown\0") } extension MemberRole: SwiftProtobuf._ProtoNameProviding { - public static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ - 0: .same(proto: "Unspecifed"), - 1: .same(proto: "TeamMember"), - 2: .same(proto: "TeamLead"), - 3: .same(proto: "HQ"), - 4: .same(proto: "Sniper"), - 5: .same(proto: "Medic"), - 6: .same(proto: "ForwardObserver"), - 7: .same(proto: "RTO"), - 8: .same(proto: "K9"), - ] + public static let _protobuf_nameMap = SwiftProtobuf._NameMap(bytecode: "\0\u{2}\0Unspecifed\0\u{1}TeamMember\0\u{1}TeamLead\0\u{1}HQ\0\u{1}Sniper\0\u{1}Medic\0\u{1}ForwardObserver\0\u{1}RTO\0\u{1}K9\0") } extension TAKPacket: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { public static let protoMessageName: String = _protobuf_package + ".TAKPacket" - public static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ - 1: .standard(proto: "is_compressed"), - 2: .same(proto: "contact"), - 3: .same(proto: "group"), - 4: .same(proto: "status"), - 5: .same(proto: "pli"), - 6: .same(proto: "chat"), - 7: .same(proto: "detail"), - ] + public static let _protobuf_nameMap = SwiftProtobuf._NameMap(bytecode: "\0\u{3}is_compressed\0\u{1}contact\0\u{1}group\0\u{1}status\0\u{1}pli\0\u{1}chat\0\u{1}detail\0") public mutating func decodeMessage(decoder: inout D) throws { while let fieldNumber = try decoder.nextFieldNumber() { @@ -674,11 +595,7 @@ extension TAKPacket: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementation extension GeoChat: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { public static let protoMessageName: String = _protobuf_package + ".GeoChat" - public static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ - 1: .same(proto: "message"), - 2: .same(proto: "to"), - 3: .standard(proto: "to_callsign"), - ] + public static let _protobuf_nameMap = SwiftProtobuf._NameMap(bytecode: "\0\u{1}message\0\u{1}to\0\u{3}to_callsign\0") public mutating func decodeMessage(decoder: inout D) throws { while let fieldNumber = try decoder.nextFieldNumber() { @@ -722,10 +639,7 @@ extension GeoChat: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBa extension Group: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { public static let protoMessageName: String = _protobuf_package + ".Group" - public static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ - 1: .same(proto: "role"), - 2: .same(proto: "team"), - ] + public static let _protobuf_nameMap = SwiftProtobuf._NameMap(bytecode: "\0\u{1}role\0\u{1}team\0") public mutating func decodeMessage(decoder: inout D) throws { while let fieldNumber = try decoder.nextFieldNumber() { @@ -760,9 +674,7 @@ extension Group: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase extension Status: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { public static let protoMessageName: String = _protobuf_package + ".Status" - public static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ - 1: .same(proto: "battery"), - ] + public static let _protobuf_nameMap = SwiftProtobuf._NameMap(bytecode: "\0\u{1}battery\0") public mutating func decodeMessage(decoder: inout D) throws { while let fieldNumber = try decoder.nextFieldNumber() { @@ -792,10 +704,7 @@ extension Status: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBas extension Contact: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { public static let protoMessageName: String = _protobuf_package + ".Contact" - public static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ - 1: .same(proto: "callsign"), - 2: .standard(proto: "device_callsign"), - ] + public static let _protobuf_nameMap = SwiftProtobuf._NameMap(bytecode: "\0\u{1}callsign\0\u{3}device_callsign\0") public mutating func decodeMessage(decoder: inout D) throws { while let fieldNumber = try decoder.nextFieldNumber() { @@ -830,13 +739,7 @@ extension Contact: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBa extension PLI: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { public static let protoMessageName: String = _protobuf_package + ".PLI" - public static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ - 1: .standard(proto: "latitude_i"), - 2: .standard(proto: "longitude_i"), - 3: .same(proto: "altitude"), - 4: .same(proto: "speed"), - 5: .same(proto: "course"), - ] + public static let _protobuf_nameMap = SwiftProtobuf._NameMap(bytecode: "\0\u{3}latitude_i\0\u{3}longitude_i\0\u{1}altitude\0\u{1}speed\0\u{1}course\0") public mutating func decodeMessage(decoder: inout D) throws { while let fieldNumber = try decoder.nextFieldNumber() { diff --git a/MeshtasticProtobufs/Sources/meshtastic/cannedmessages.pb.swift b/MeshtasticProtobufs/Sources/meshtastic/cannedmessages.pb.swift index 1b8c84de..8776adbf 100644 --- a/MeshtasticProtobufs/Sources/meshtastic/cannedmessages.pb.swift +++ b/MeshtasticProtobufs/Sources/meshtastic/cannedmessages.pb.swift @@ -1,5 +1,6 @@ // DO NOT EDIT. // swift-format-ignore-file +// swiftlint:disable all // // Generated by the Swift generator plugin for the protocol buffer compiler. // Source: meshtastic/cannedmessages.proto @@ -7,7 +8,6 @@ // For information on using the generated types, please see the documentation: // https://github.com/apple/swift-protobuf/ -import Foundation import SwiftProtobuf // If the compiler emits an error on this type, it is because this file @@ -22,7 +22,7 @@ fileprivate struct _GeneratedWithProtocGenSwiftVersion: SwiftProtobuf.ProtobufAP /// /// Canned message module configuration. -public struct CannedMessageModuleConfig { +public struct CannedMessageModuleConfig: 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. @@ -36,19 +36,13 @@ public struct CannedMessageModuleConfig { public init() {} } -#if swift(>=5.5) && canImport(_Concurrency) -extension CannedMessageModuleConfig: @unchecked Sendable {} -#endif // swift(>=5.5) && canImport(_Concurrency) - // MARK: - Code below here is support for the SwiftProtobuf runtime. fileprivate let _protobuf_package = "meshtastic" extension CannedMessageModuleConfig: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { public static let protoMessageName: String = _protobuf_package + ".CannedMessageModuleConfig" - public static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ - 1: .same(proto: "messages"), - ] + public static let _protobuf_nameMap = SwiftProtobuf._NameMap(bytecode: "\0\u{1}messages\0") public mutating func decodeMessage(decoder: inout D) throws { while let fieldNumber = try decoder.nextFieldNumber() { diff --git a/MeshtasticProtobufs/Sources/meshtastic/channel.pb.swift b/MeshtasticProtobufs/Sources/meshtastic/channel.pb.swift index 9af4a87a..15995842 100644 --- a/MeshtasticProtobufs/Sources/meshtastic/channel.pb.swift +++ b/MeshtasticProtobufs/Sources/meshtastic/channel.pb.swift @@ -1,5 +1,6 @@ // DO NOT EDIT. // swift-format-ignore-file +// swiftlint:disable all // // Generated by the Swift generator plugin for the protocol buffer compiler. // Source: meshtastic/channel.proto @@ -36,13 +37,15 @@ fileprivate struct _GeneratedWithProtocGenSwiftVersion: SwiftProtobuf.ProtobufAP /// FIXME: Add description of multi-channel support and how primary vs secondary channels are used. /// FIXME: explain how apps use channels for security. /// explain how remote settings and remote gpio are managed as an example -public struct ChannelSettings { +public struct ChannelSettings: 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. /// /// Deprecated in favor of LoraConfig.channel_num + /// + /// NOTE: This field was marked as deprecated in the .proto file. public var channelNum: UInt32 = 0 /// @@ -102,10 +105,6 @@ public struct ChannelSettings { /// Clears the value of `moduleSettings`. Subsequent reads from it will return its default value. public mutating func clearModuleSettings() {self._moduleSettings = nil} - /// - /// Whether or not we should receive notifactions / alerts through this channel - public var mute: Bool = false - public var unknownFields = SwiftProtobuf.UnknownStorage() public init() {} @@ -115,7 +114,7 @@ public struct ChannelSettings { /// /// This message is specifically for modules to store per-channel configuration data. -public struct ModuleSettings { +public struct ModuleSettings: 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. @@ -125,9 +124,9 @@ public struct ModuleSettings { public var positionPrecision: UInt32 = 0 /// - /// Controls whether or not the phone / clients should mute the current channel + /// Controls whether or not the client / device should mute the current channel /// Useful for noisy public channels you don't necessarily want to disable - public var isClientMuted: Bool = false + public var isMuted: Bool = false public var unknownFields = SwiftProtobuf.UnknownStorage() @@ -136,7 +135,7 @@ public struct ModuleSettings { /// /// A pair of a channel number, mode and the (sharable) settings for that channel -public struct Channel { +public struct Channel: 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. @@ -174,7 +173,7 @@ public struct Channel { /// cross band routing as needed. /// If a device has only a single radio (the common case) only one channel can be PRIMARY at a time /// (but any number of SECONDARY channels can't be sent received on that common frequency) - public enum Role: SwiftProtobuf.Enum { + public enum Role: SwiftProtobuf.Enum, Swift.CaseIterable { public typealias RawValue = Int /// @@ -213,6 +212,13 @@ public struct Channel { } } + // The compiler won't synthesize support with the UNRECOGNIZED case. + public static let allCases: [Channel.Role] = [ + .disabled, + .primary, + .secondary, + ] + } public init() {} @@ -220,42 +226,13 @@ public struct Channel { fileprivate var _settings: ChannelSettings? = nil } -#if swift(>=4.2) - -extension Channel.Role: CaseIterable { - // The compiler won't synthesize support with the UNRECOGNIZED case. - public static let allCases: [Channel.Role] = [ - .disabled, - .primary, - .secondary, - ] -} - -#endif // swift(>=4.2) - -#if swift(>=5.5) && canImport(_Concurrency) -extension ChannelSettings: @unchecked Sendable {} -extension ModuleSettings: @unchecked Sendable {} -extension Channel: @unchecked Sendable {} -extension Channel.Role: @unchecked Sendable {} -#endif // swift(>=5.5) && canImport(_Concurrency) - // MARK: - Code below here is support for the SwiftProtobuf runtime. fileprivate let _protobuf_package = "meshtastic" extension ChannelSettings: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { public static let protoMessageName: String = _protobuf_package + ".ChannelSettings" - public static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ - 1: .standard(proto: "channel_num"), - 2: .same(proto: "psk"), - 3: .same(proto: "name"), - 4: .same(proto: "id"), - 5: .standard(proto: "uplink_enabled"), - 6: .standard(proto: "downlink_enabled"), - 7: .standard(proto: "module_settings"), - 8: .same(proto: "mute"), - ] + public static let _protobuf_nameMap = SwiftProtobuf._NameMap(bytecode: "\0\u{3}channel_num\0\u{1}psk\0\u{1}name\0\u{1}id\0\u{3}uplink_enabled\0\u{3}downlink_enabled\0\u{3}module_settings\0") public mutating func decodeMessage(decoder: inout D) throws { while let fieldNumber = try decoder.nextFieldNumber() { @@ -270,7 +247,6 @@ extension ChannelSettings: SwiftProtobuf.Message, SwiftProtobuf._MessageImplemen case 5: try { try decoder.decodeSingularBoolField(value: &self.uplinkEnabled) }() case 6: try { try decoder.decodeSingularBoolField(value: &self.downlinkEnabled) }() case 7: try { try decoder.decodeSingularMessageField(value: &self._moduleSettings) }() - case 8: try { try decoder.decodeSingularBoolField(value: &self.mute) }() default: break } } @@ -302,9 +278,6 @@ extension ChannelSettings: SwiftProtobuf.Message, SwiftProtobuf._MessageImplemen try { if let v = self._moduleSettings { try visitor.visitSingularMessageField(value: v, fieldNumber: 7) } }() - if self.mute != false { - try visitor.visitSingularBoolField(value: self.mute, fieldNumber: 8) - } try unknownFields.traverse(visitor: &visitor) } @@ -316,7 +289,6 @@ extension ChannelSettings: SwiftProtobuf.Message, SwiftProtobuf._MessageImplemen if lhs.uplinkEnabled != rhs.uplinkEnabled {return false} if lhs.downlinkEnabled != rhs.downlinkEnabled {return false} if lhs._moduleSettings != rhs._moduleSettings {return false} - if lhs.mute != rhs.mute {return false} if lhs.unknownFields != rhs.unknownFields {return false} return true } @@ -324,10 +296,7 @@ extension ChannelSettings: SwiftProtobuf.Message, SwiftProtobuf._MessageImplemen extension ModuleSettings: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { public static let protoMessageName: String = _protobuf_package + ".ModuleSettings" - public static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ - 1: .standard(proto: "position_precision"), - 2: .standard(proto: "is_client_muted"), - ] + public static let _protobuf_nameMap = SwiftProtobuf._NameMap(bytecode: "\0\u{3}position_precision\0\u{3}is_muted\0") public mutating func decodeMessage(decoder: inout D) throws { while let fieldNumber = try decoder.nextFieldNumber() { @@ -336,7 +305,7 @@ extension ModuleSettings: SwiftProtobuf.Message, SwiftProtobuf._MessageImplement // enabled. https://github.com/apple/swift-protobuf/issues/1034 switch fieldNumber { case 1: try { try decoder.decodeSingularUInt32Field(value: &self.positionPrecision) }() - case 2: try { try decoder.decodeSingularBoolField(value: &self.isClientMuted) }() + case 2: try { try decoder.decodeSingularBoolField(value: &self.isMuted) }() default: break } } @@ -346,15 +315,15 @@ extension ModuleSettings: SwiftProtobuf.Message, SwiftProtobuf._MessageImplement if self.positionPrecision != 0 { try visitor.visitSingularUInt32Field(value: self.positionPrecision, fieldNumber: 1) } - if self.isClientMuted != false { - try visitor.visitSingularBoolField(value: self.isClientMuted, fieldNumber: 2) + if self.isMuted != false { + try visitor.visitSingularBoolField(value: self.isMuted, fieldNumber: 2) } try unknownFields.traverse(visitor: &visitor) } public static func ==(lhs: ModuleSettings, rhs: ModuleSettings) -> Bool { if lhs.positionPrecision != rhs.positionPrecision {return false} - if lhs.isClientMuted != rhs.isClientMuted {return false} + if lhs.isMuted != rhs.isMuted {return false} if lhs.unknownFields != rhs.unknownFields {return false} return true } @@ -362,11 +331,7 @@ extension ModuleSettings: SwiftProtobuf.Message, SwiftProtobuf._MessageImplement extension Channel: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { public static let protoMessageName: String = _protobuf_package + ".Channel" - public static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ - 1: .same(proto: "index"), - 2: .same(proto: "settings"), - 3: .same(proto: "role"), - ] + public static let _protobuf_nameMap = SwiftProtobuf._NameMap(bytecode: "\0\u{1}index\0\u{1}settings\0\u{1}role\0") public mutating func decodeMessage(decoder: inout D) throws { while let fieldNumber = try decoder.nextFieldNumber() { @@ -409,9 +374,5 @@ extension Channel: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBa } extension Channel.Role: SwiftProtobuf._ProtoNameProviding { - public static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ - 0: .same(proto: "DISABLED"), - 1: .same(proto: "PRIMARY"), - 2: .same(proto: "SECONDARY"), - ] + public static let _protobuf_nameMap = SwiftProtobuf._NameMap(bytecode: "\0\u{2}\0DISABLED\0\u{1}PRIMARY\0\u{1}SECONDARY\0") } diff --git a/MeshtasticProtobufs/Sources/meshtastic/clientonly.pb.swift b/MeshtasticProtobufs/Sources/meshtastic/clientonly.pb.swift index f89a8e3c..9afbdf9c 100644 --- a/MeshtasticProtobufs/Sources/meshtastic/clientonly.pb.swift +++ b/MeshtasticProtobufs/Sources/meshtastic/clientonly.pb.swift @@ -1,5 +1,6 @@ // DO NOT EDIT. // swift-format-ignore-file +// swiftlint:disable all // // Generated by the Swift generator plugin for the protocol buffer compiler. // Source: meshtastic/clientonly.proto @@ -7,7 +8,6 @@ // For information on using the generated types, please see the documentation: // https://github.com/apple/swift-protobuf/ -import Foundation import SwiftProtobuf // If the compiler emits an error on this type, it is because this file @@ -23,7 +23,7 @@ fileprivate struct _GeneratedWithProtocGenSwiftVersion: SwiftProtobuf.ProtobufAP /// /// This abstraction is used to contain any configuration for provisioning a node on any client. /// It is useful for importing and exporting configurations. -public struct DeviceProfile { +public struct DeviceProfile: 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. @@ -130,26 +130,13 @@ public struct DeviceProfile { fileprivate var _cannedMessages: String? = nil } -#if swift(>=5.5) && canImport(_Concurrency) -extension DeviceProfile: @unchecked Sendable {} -#endif // swift(>=5.5) && canImport(_Concurrency) - // MARK: - Code below here is support for the SwiftProtobuf runtime. fileprivate let _protobuf_package = "meshtastic" extension DeviceProfile: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { public static let protoMessageName: String = _protobuf_package + ".DeviceProfile" - public static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ - 1: .standard(proto: "long_name"), - 2: .standard(proto: "short_name"), - 3: .standard(proto: "channel_url"), - 4: .same(proto: "config"), - 5: .standard(proto: "module_config"), - 6: .standard(proto: "fixed_position"), - 7: .same(proto: "ringtone"), - 8: .standard(proto: "canned_messages"), - ] + public static let _protobuf_nameMap = SwiftProtobuf._NameMap(bytecode: "\0\u{3}long_name\0\u{3}short_name\0\u{3}channel_url\0\u{1}config\0\u{3}module_config\0\u{3}fixed_position\0\u{1}ringtone\0\u{3}canned_messages\0") public mutating func decodeMessage(decoder: inout D) throws { while let fieldNumber = try decoder.nextFieldNumber() { diff --git a/MeshtasticProtobufs/Sources/meshtastic/config.pb.swift b/MeshtasticProtobufs/Sources/meshtastic/config.pb.swift index 03862795..28074a6b 100644 --- a/MeshtasticProtobufs/Sources/meshtastic/config.pb.swift +++ b/MeshtasticProtobufs/Sources/meshtastic/config.pb.swift @@ -1,5 +1,6 @@ // DO NOT EDIT. // swift-format-ignore-file +// swiftlint:disable all // // Generated by the Swift generator plugin for the protocol buffer compiler. // Source: meshtastic/config.proto @@ -20,7 +21,7 @@ fileprivate struct _GeneratedWithProtocGenSwiftVersion: SwiftProtobuf.ProtobufAP typealias Version = _2 } -public struct Config { +public struct Config: 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. @@ -113,7 +114,7 @@ public struct Config { /// /// Payload Variant - public enum OneOf_PayloadVariant: Equatable { + public enum OneOf_PayloadVariant: Equatable, Sendable { case device(Config.DeviceConfig) case position(Config.PositionConfig) case power(Config.PowerConfig) @@ -125,61 +126,11 @@ public struct Config { case sessionkey(Config.SessionkeyConfig) case deviceUi(DeviceUIConfig) - #if !swift(>=4.1) - public static func ==(lhs: Config.OneOf_PayloadVariant, rhs: Config.OneOf_PayloadVariant) -> Bool { - // 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 (lhs, rhs) { - case (.device, .device): return { - guard case .device(let l) = lhs, case .device(let r) = rhs else { preconditionFailure() } - return l == r - }() - case (.position, .position): return { - guard case .position(let l) = lhs, case .position(let r) = rhs else { preconditionFailure() } - return l == r - }() - case (.power, .power): return { - guard case .power(let l) = lhs, case .power(let r) = rhs else { preconditionFailure() } - return l == r - }() - case (.network, .network): return { - guard case .network(let l) = lhs, case .network(let r) = rhs else { preconditionFailure() } - return l == r - }() - case (.display, .display): return { - guard case .display(let l) = lhs, case .display(let r) = rhs else { preconditionFailure() } - return l == r - }() - case (.lora, .lora): return { - guard case .lora(let l) = lhs, case .lora(let r) = rhs else { preconditionFailure() } - return l == r - }() - case (.bluetooth, .bluetooth): return { - guard case .bluetooth(let l) = lhs, case .bluetooth(let r) = rhs else { preconditionFailure() } - return l == r - }() - case (.security, .security): return { - guard case .security(let l) = lhs, case .security(let r) = rhs else { preconditionFailure() } - return l == r - }() - case (.sessionkey, .sessionkey): return { - guard case .sessionkey(let l) = lhs, case .sessionkey(let r) = rhs else { preconditionFailure() } - return l == r - }() - case (.deviceUi, .deviceUi): return { - guard case .deviceUi(let l) = lhs, case .deviceUi(let r) = rhs else { preconditionFailure() } - return l == r - }() - default: return false - } - } - #endif } /// /// Configuration - public struct DeviceConfig { + public struct DeviceConfig: 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. @@ -191,6 +142,8 @@ public struct Config { /// /// Disabling this will disable the SerialConsole by not initilizing the StreamAPI /// Moved to SecurityConfig + /// + /// NOTE: This field was marked as deprecated in the .proto file. public var serialEnabled: Bool = false /// @@ -220,6 +173,8 @@ public struct Config { /// If true, device is considered to be "managed" by a mesh administrator /// Clients should then limit available configuration and administrative options inside the user interface /// Moved to SecurityConfig + /// + /// NOTE: This field was marked as deprecated in the .proto file. public var isManaged: Bool = false /// @@ -243,7 +198,7 @@ public struct Config { /// /// Defines the device's role on the Mesh network - public enum Role: SwiftProtobuf.Enum { + public enum Role: SwiftProtobuf.Enum, Swift.CaseIterable { public typealias RawValue = Int /// @@ -261,6 +216,8 @@ public struct Config { /// The wifi radio and the oled screen will be put to sleep. /// This mode may still potentially have higher power usage due to it's preference in message rebroadcasting on the mesh. case router // = 2 + + /// NOTE: This enum value was marked as deprecated in the .proto file case routerClient // = 3 /// @@ -268,6 +225,8 @@ public struct Config { /// Technical Details: Mesh packets will simply be rebroadcasted over this node. Nodes configured with this role will not originate NodeInfo, Position, Telemetry /// or any other packet type. They will simply rebroadcast any mesh packets on the same frequency, channel num, spread factor, and coding rate. /// Deprecated in v2.7.11 because it creates "holes" in the mesh rebroadcast chain. + /// + /// NOTE: This enum value was marked as deprecated in the .proto file case repeater // = 4 /// @@ -371,11 +330,28 @@ public struct Config { } } + // The compiler won't synthesize support with the UNRECOGNIZED case. + public static let allCases: [Config.DeviceConfig.Role] = [ + .client, + .clientMute, + .router, + .routerClient, + .repeater, + .tracker, + .sensor, + .tak, + .clientHidden, + .lostAndFound, + .takTracker, + .routerLate, + .clientBase, + ] + } /// /// Defines the device's behavior for how messages are rebroadcast - public enum RebroadcastMode: SwiftProtobuf.Enum { + public enum RebroadcastMode: SwiftProtobuf.Enum, Swift.CaseIterable { public typealias RawValue = Int /// @@ -436,11 +412,21 @@ public struct Config { } } + // The compiler won't synthesize support with the UNRECOGNIZED case. + public static let allCases: [Config.DeviceConfig.RebroadcastMode] = [ + .all, + .allSkipDecoding, + .localOnly, + .knownOnly, + .none, + .corePortnumsOnly, + ] + } /// /// Defines buzzer behavior for audio feedback - public enum BuzzerMode: SwiftProtobuf.Enum { + public enum BuzzerMode: SwiftProtobuf.Enum, Swift.CaseIterable { public typealias RawValue = Int /// @@ -497,6 +483,15 @@ public struct Config { } } + // The compiler won't synthesize support with the UNRECOGNIZED case. + public static let allCases: [Config.DeviceConfig.BuzzerMode] = [ + .allEnabled, + .disabled, + .notificationsOnly, + .systemOnly, + .directMsgOnly, + ] + } public init() {} @@ -504,7 +499,7 @@ public struct Config { /// /// Position Config - public struct PositionConfig { + public struct PositionConfig: 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. @@ -526,6 +521,8 @@ public struct Config { /// /// Is GPS enabled for this node? + /// + /// NOTE: This field was marked as deprecated in the .proto file. public var gpsEnabled: Bool = false /// @@ -536,6 +533,8 @@ public struct Config { /// /// Deprecated in favor of using smart / regular broadcast intervals as implicit attempt time + /// + /// NOTE: This field was marked as deprecated in the .proto file. public var gpsAttemptTime: UInt32 = 0 /// @@ -576,7 +575,7 @@ public struct Config { /// are always included (also time if GPS-synced) /// NOTE: the more fields are included, the larger the message will be - /// leading to longer airtime and a higher risk of packet loss - public enum PositionFlags: SwiftProtobuf.Enum { + public enum PositionFlags: SwiftProtobuf.Enum, Swift.CaseIterable { public typealias RawValue = Int /// @@ -666,9 +665,24 @@ public struct Config { } } + // The compiler won't synthesize support with the UNRECOGNIZED case. + public static let allCases: [Config.PositionConfig.PositionFlags] = [ + .unset, + .altitude, + .altitudeMsl, + .geoidalSeparation, + .dop, + .hvdop, + .satinview, + .seqNo, + .timestamp, + .heading, + .speed, + ] + } - public enum GpsMode: SwiftProtobuf.Enum { + public enum GpsMode: SwiftProtobuf.Enum, Swift.CaseIterable { public typealias RawValue = Int /// @@ -706,6 +720,13 @@ public struct Config { } } + // The compiler won't synthesize support with the UNRECOGNIZED case. + public static let allCases: [Config.PositionConfig.GpsMode] = [ + .disabled, + .enabled, + .notPresent, + ] + } public init() {} @@ -714,7 +735,7 @@ public struct Config { /// /// Power Config\ /// See [Power Config](/docs/settings/config/power) for additional power config details. - public struct PowerConfig { + public struct PowerConfig: 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. @@ -774,7 +795,7 @@ public struct Config { /// /// Network Config - public struct NetworkConfig { + public struct NetworkConfig: 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. @@ -829,7 +850,7 @@ public struct Config { public var unknownFields = SwiftProtobuf.UnknownStorage() - public enum AddressMode: SwiftProtobuf.Enum { + public enum AddressMode: SwiftProtobuf.Enum, Swift.CaseIterable { public typealias RawValue = Int /// @@ -861,11 +882,17 @@ public struct Config { } } + // The compiler won't synthesize support with the UNRECOGNIZED case. + public static let allCases: [Config.NetworkConfig.AddressMode] = [ + .dhcp, + .static, + ] + } /// /// Available flags auxiliary network protocols - public enum ProtocolFlags: SwiftProtobuf.Enum { + public enum ProtocolFlags: SwiftProtobuf.Enum, Swift.CaseIterable { public typealias RawValue = Int /// @@ -897,9 +924,15 @@ public struct Config { } } + // The compiler won't synthesize support with the UNRECOGNIZED case. + public static let allCases: [Config.NetworkConfig.ProtocolFlags] = [ + .noBroadcast, + .udpBroadcast, + ] + } - public struct IpV4Config { + public struct IpV4Config: 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. @@ -932,7 +965,7 @@ public struct Config { /// /// Display Config - public struct DisplayConfig { + public struct DisplayConfig: 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. @@ -945,6 +978,8 @@ public struct Config { /// /// Deprecated in 2.7.4: Unused /// How the GPS coordinates are formatted on the OLED screen. + /// + /// NOTE: This field was marked as deprecated in the .proto file. public var gpsFormat: Config.DisplayConfig.DeprecatedGpsCoordinateFormat = .unused /// @@ -955,6 +990,8 @@ public struct Config { /// /// If this is set, the displayed compass will always point north. if unset, the old behaviour /// (top of display is heading direction) is used. + /// + /// NOTE: This field was marked as deprecated in the .proto file. public var compassNorthTop: Bool = false /// @@ -999,7 +1036,7 @@ public struct Config { /// /// Deprecated in 2.7.4: Unused - public enum DeprecatedGpsCoordinateFormat: SwiftProtobuf.Enum { + public enum DeprecatedGpsCoordinateFormat: SwiftProtobuf.Enum, Swift.CaseIterable { public typealias RawValue = Int case unused // = 0 case UNRECOGNIZED(Int) @@ -1022,11 +1059,16 @@ public struct Config { } } + // The compiler won't synthesize support with the UNRECOGNIZED case. + public static let allCases: [Config.DisplayConfig.DeprecatedGpsCoordinateFormat] = [ + .unused, + ] + } /// /// Unit display preference - public enum DisplayUnits: SwiftProtobuf.Enum { + public enum DisplayUnits: SwiftProtobuf.Enum, Swift.CaseIterable { public typealias RawValue = Int /// @@ -1058,11 +1100,17 @@ public struct Config { } } + // The compiler won't synthesize support with the UNRECOGNIZED case. + public static let allCases: [Config.DisplayConfig.DisplayUnits] = [ + .metric, + .imperial, + ] + } /// /// Override OLED outo detect with this if it fails. - public enum OledType: SwiftProtobuf.Enum { + public enum OledType: SwiftProtobuf.Enum, Swift.CaseIterable { public typealias RawValue = Int /// @@ -1112,9 +1160,18 @@ public struct Config { } } + // The compiler won't synthesize support with the UNRECOGNIZED case. + public static let allCases: [Config.DisplayConfig.OledType] = [ + .oledAuto, + .oledSsd1306, + .oledSh1106, + .oledSh1107, + .oledSh1107128128, + ] + } - public enum DisplayMode: SwiftProtobuf.Enum { + public enum DisplayMode: SwiftProtobuf.Enum, Swift.CaseIterable { public typealias RawValue = Int /// @@ -1158,9 +1215,17 @@ public struct Config { } } + // The compiler won't synthesize support with the UNRECOGNIZED case. + public static let allCases: [Config.DisplayConfig.DisplayMode] = [ + .default, + .twocolor, + .inverted, + .color, + ] + } - public enum CompassOrientation: SwiftProtobuf.Enum { + public enum CompassOrientation: SwiftProtobuf.Enum, Swift.CaseIterable { public typealias RawValue = Int /// @@ -1228,6 +1293,18 @@ public struct Config { } } + // The compiler won't synthesize support with the UNRECOGNIZED case. + public static let allCases: [Config.DisplayConfig.CompassOrientation] = [ + .degrees0, + .degrees90, + .degrees180, + .degrees270, + .degrees0Inverted, + .degrees90Inverted, + .degrees180Inverted, + .degrees270Inverted, + ] + } public init() {} @@ -1235,7 +1312,7 @@ public struct Config { /// /// Lora Config - public struct LoRaConfig { + public struct LoRaConfig: @unchecked 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. @@ -1399,7 +1476,7 @@ public struct Config { public var unknownFields = SwiftProtobuf.UnknownStorage() - public enum RegionCode: SwiftProtobuf.Enum { + public enum RegionCode: SwiftProtobuf.Enum, Swift.CaseIterable { public typealias RawValue = Int /// @@ -1581,12 +1658,43 @@ public struct Config { } } + // The compiler won't synthesize support with the UNRECOGNIZED case. + public static let allCases: [Config.LoRaConfig.RegionCode] = [ + .unset, + .us, + .eu433, + .eu868, + .cn, + .jp, + .anz, + .kr, + .tw, + .ru, + .in, + .nz865, + .th, + .lora24, + .ua433, + .ua868, + .my433, + .my919, + .sg923, + .ph433, + .ph868, + .ph915, + .anz433, + .kz433, + .kz863, + .np865, + .br902, + ] + } /// /// Standard predefined channel settings /// Note: these mappings must match ModemPreset Choice in the device code. - public enum ModemPreset: SwiftProtobuf.Enum { + public enum ModemPreset: SwiftProtobuf.Enum, Swift.CaseIterable { public typealias RawValue = Int /// @@ -1595,11 +1703,16 @@ public struct Config { /// /// Long Range - Slow + /// Deprecated in 2.7: Unpopular slow preset. + /// + /// NOTE: This enum value was marked as deprecated in the .proto file case longSlow // = 1 /// /// Very Long Range - Slow /// Deprecated in 2.5: Works only with txco and is unusably slow + /// + /// NOTE: This enum value was marked as deprecated in the .proto file case veryLongSlow // = 2 /// @@ -1627,6 +1740,11 @@ public struct Config { /// This is the fastest preset and the only one with 500kHz bandwidth. /// It is not legal to use in all regions due to this wider bandwidth. case shortTurbo // = 8 + + /// + /// Long Range - Turbo + /// This preset performs similarly to LongFast, but with 500Khz bandwidth. + case longTurbo // = 9 case UNRECOGNIZED(Int) public init() { @@ -1644,6 +1762,7 @@ public struct Config { case 6: self = .shortFast case 7: self = .longModerate case 8: self = .shortTurbo + case 9: self = .longTurbo default: self = .UNRECOGNIZED(rawValue) } } @@ -1659,10 +1778,25 @@ public struct Config { case .shortFast: return 6 case .longModerate: return 7 case .shortTurbo: return 8 + case .longTurbo: return 9 case .UNRECOGNIZED(let i): return i } } + // The compiler won't synthesize support with the UNRECOGNIZED case. + public static let allCases: [Config.LoRaConfig.ModemPreset] = [ + .longFast, + .longSlow, + .veryLongSlow, + .mediumSlow, + .mediumFast, + .shortSlow, + .shortFast, + .longModerate, + .shortTurbo, + .longTurbo, + ] + } public init() {} @@ -1670,7 +1804,7 @@ public struct Config { fileprivate var _storage = _StorageClass.defaultInstance } - public struct BluetoothConfig { + public struct BluetoothConfig: 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. @@ -1689,7 +1823,7 @@ public struct Config { public var unknownFields = SwiftProtobuf.UnknownStorage() - public enum PairingMode: SwiftProtobuf.Enum { + public enum PairingMode: SwiftProtobuf.Enum, Swift.CaseIterable { public typealias RawValue = Int /// @@ -1727,12 +1861,19 @@ public struct Config { } } + // The compiler won't synthesize support with the UNRECOGNIZED case. + public static let allCases: [Config.BluetoothConfig.PairingMode] = [ + .randomPin, + .fixedPin, + .noPin, + ] + } public init() {} } - public struct SecurityConfig { + public struct SecurityConfig: 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. @@ -1776,7 +1917,7 @@ public struct Config { /// /// Blank config request, strictly for getting the session key - public struct SessionkeyConfig { + public struct SessionkeyConfig: 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. @@ -1789,249 +1930,13 @@ public struct Config { public init() {} } -#if swift(>=4.2) - -extension Config.DeviceConfig.Role: CaseIterable { - // The compiler won't synthesize support with the UNRECOGNIZED case. - public static let allCases: [Config.DeviceConfig.Role] = [ - .client, - .clientMute, - .router, - .routerClient, - .repeater, - .tracker, - .sensor, - .tak, - .clientHidden, - .lostAndFound, - .takTracker, - .routerLate, - .clientBase, - ] -} - -extension Config.DeviceConfig.RebroadcastMode: CaseIterable { - // The compiler won't synthesize support with the UNRECOGNIZED case. - public static let allCases: [Config.DeviceConfig.RebroadcastMode] = [ - .all, - .allSkipDecoding, - .localOnly, - .knownOnly, - .none, - .corePortnumsOnly, - ] -} - -extension Config.DeviceConfig.BuzzerMode: CaseIterable { - // The compiler won't synthesize support with the UNRECOGNIZED case. - public static let allCases: [Config.DeviceConfig.BuzzerMode] = [ - .allEnabled, - .disabled, - .notificationsOnly, - .systemOnly, - .directMsgOnly, - ] -} - -extension Config.PositionConfig.PositionFlags: CaseIterable { - // The compiler won't synthesize support with the UNRECOGNIZED case. - public static let allCases: [Config.PositionConfig.PositionFlags] = [ - .unset, - .altitude, - .altitudeMsl, - .geoidalSeparation, - .dop, - .hvdop, - .satinview, - .seqNo, - .timestamp, - .heading, - .speed, - ] -} - -extension Config.PositionConfig.GpsMode: CaseIterable { - // The compiler won't synthesize support with the UNRECOGNIZED case. - public static let allCases: [Config.PositionConfig.GpsMode] = [ - .disabled, - .enabled, - .notPresent, - ] -} - -extension Config.NetworkConfig.AddressMode: CaseIterable { - // The compiler won't synthesize support with the UNRECOGNIZED case. - public static let allCases: [Config.NetworkConfig.AddressMode] = [ - .dhcp, - .static, - ] -} - -extension Config.NetworkConfig.ProtocolFlags: CaseIterable { - // The compiler won't synthesize support with the UNRECOGNIZED case. - public static let allCases: [Config.NetworkConfig.ProtocolFlags] = [ - .noBroadcast, - .udpBroadcast, - ] -} - -extension Config.DisplayConfig.DeprecatedGpsCoordinateFormat: CaseIterable { - // The compiler won't synthesize support with the UNRECOGNIZED case. - public static let allCases: [Config.DisplayConfig.DeprecatedGpsCoordinateFormat] = [ - .unused, - ] -} - -extension Config.DisplayConfig.DisplayUnits: CaseIterable { - // The compiler won't synthesize support with the UNRECOGNIZED case. - public static let allCases: [Config.DisplayConfig.DisplayUnits] = [ - .metric, - .imperial, - ] -} - -extension Config.DisplayConfig.OledType: CaseIterable { - // The compiler won't synthesize support with the UNRECOGNIZED case. - public static let allCases: [Config.DisplayConfig.OledType] = [ - .oledAuto, - .oledSsd1306, - .oledSh1106, - .oledSh1107, - .oledSh1107128128, - ] -} - -extension Config.DisplayConfig.DisplayMode: CaseIterable { - // The compiler won't synthesize support with the UNRECOGNIZED case. - public static let allCases: [Config.DisplayConfig.DisplayMode] = [ - .default, - .twocolor, - .inverted, - .color, - ] -} - -extension Config.DisplayConfig.CompassOrientation: CaseIterable { - // The compiler won't synthesize support with the UNRECOGNIZED case. - public static let allCases: [Config.DisplayConfig.CompassOrientation] = [ - .degrees0, - .degrees90, - .degrees180, - .degrees270, - .degrees0Inverted, - .degrees90Inverted, - .degrees180Inverted, - .degrees270Inverted, - ] -} - -extension Config.LoRaConfig.RegionCode: CaseIterable { - // The compiler won't synthesize support with the UNRECOGNIZED case. - public static let allCases: [Config.LoRaConfig.RegionCode] = [ - .unset, - .us, - .eu433, - .eu868, - .cn, - .jp, - .anz, - .kr, - .tw, - .ru, - .in, - .nz865, - .th, - .lora24, - .ua433, - .ua868, - .my433, - .my919, - .sg923, - .ph433, - .ph868, - .ph915, - .anz433, - .kz433, - .kz863, - .np865, - .br902, - ] -} - -extension Config.LoRaConfig.ModemPreset: CaseIterable { - // The compiler won't synthesize support with the UNRECOGNIZED case. - public static let allCases: [Config.LoRaConfig.ModemPreset] = [ - .longFast, - .longSlow, - .veryLongSlow, - .mediumSlow, - .mediumFast, - .shortSlow, - .shortFast, - .longModerate, - .shortTurbo, - ] -} - -extension Config.BluetoothConfig.PairingMode: CaseIterable { - // The compiler won't synthesize support with the UNRECOGNIZED case. - public static let allCases: [Config.BluetoothConfig.PairingMode] = [ - .randomPin, - .fixedPin, - .noPin, - ] -} - -#endif // swift(>=4.2) - -#if swift(>=5.5) && canImport(_Concurrency) -extension Config: @unchecked Sendable {} -extension Config.OneOf_PayloadVariant: @unchecked Sendable {} -extension Config.DeviceConfig: @unchecked Sendable {} -extension Config.DeviceConfig.Role: @unchecked Sendable {} -extension Config.DeviceConfig.RebroadcastMode: @unchecked Sendable {} -extension Config.DeviceConfig.BuzzerMode: @unchecked Sendable {} -extension Config.PositionConfig: @unchecked Sendable {} -extension Config.PositionConfig.PositionFlags: @unchecked Sendable {} -extension Config.PositionConfig.GpsMode: @unchecked Sendable {} -extension Config.PowerConfig: @unchecked Sendable {} -extension Config.NetworkConfig: @unchecked Sendable {} -extension Config.NetworkConfig.AddressMode: @unchecked Sendable {} -extension Config.NetworkConfig.ProtocolFlags: @unchecked Sendable {} -extension Config.NetworkConfig.IpV4Config: @unchecked Sendable {} -extension Config.DisplayConfig: @unchecked Sendable {} -extension Config.DisplayConfig.DeprecatedGpsCoordinateFormat: @unchecked Sendable {} -extension Config.DisplayConfig.DisplayUnits: @unchecked Sendable {} -extension Config.DisplayConfig.OledType: @unchecked Sendable {} -extension Config.DisplayConfig.DisplayMode: @unchecked Sendable {} -extension Config.DisplayConfig.CompassOrientation: @unchecked Sendable {} -extension Config.LoRaConfig: @unchecked Sendable {} -extension Config.LoRaConfig.RegionCode: @unchecked Sendable {} -extension Config.LoRaConfig.ModemPreset: @unchecked Sendable {} -extension Config.BluetoothConfig: @unchecked Sendable {} -extension Config.BluetoothConfig.PairingMode: @unchecked Sendable {} -extension Config.SecurityConfig: @unchecked Sendable {} -extension Config.SessionkeyConfig: @unchecked Sendable {} -#endif // swift(>=5.5) && canImport(_Concurrency) - // MARK: - Code below here is support for the SwiftProtobuf runtime. fileprivate let _protobuf_package = "meshtastic" extension Config: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { public static let protoMessageName: String = _protobuf_package + ".Config" - public static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ - 1: .same(proto: "device"), - 2: .same(proto: "position"), - 3: .same(proto: "power"), - 4: .same(proto: "network"), - 5: .same(proto: "display"), - 6: .same(proto: "lora"), - 7: .same(proto: "bluetooth"), - 8: .same(proto: "security"), - 9: .same(proto: "sessionkey"), - 10: .standard(proto: "device_ui"), - ] + public static let _protobuf_nameMap = SwiftProtobuf._NameMap(bytecode: "\0\u{1}device\0\u{1}position\0\u{1}power\0\u{1}network\0\u{1}display\0\u{1}lora\0\u{1}bluetooth\0\u{1}security\0\u{1}sessionkey\0\u{3}device_ui\0") public mutating func decodeMessage(decoder: inout D) throws { while let fieldNumber = try decoder.nextFieldNumber() { @@ -2234,20 +2139,7 @@ extension Config: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBas extension Config.DeviceConfig: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { public static let protoMessageName: String = Config.protoMessageName + ".DeviceConfig" - public static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ - 1: .same(proto: "role"), - 2: .standard(proto: "serial_enabled"), - 4: .standard(proto: "button_gpio"), - 5: .standard(proto: "buzzer_gpio"), - 6: .standard(proto: "rebroadcast_mode"), - 7: .standard(proto: "node_info_broadcast_secs"), - 8: .standard(proto: "double_tap_as_button_press"), - 9: .standard(proto: "is_managed"), - 10: .standard(proto: "disable_triple_click"), - 11: .same(proto: "tzdef"), - 12: .standard(proto: "led_heartbeat_disabled"), - 13: .standard(proto: "buzzer_mode"), - ] + public static let _protobuf_nameMap = SwiftProtobuf._NameMap(bytecode: "\0\u{1}role\0\u{3}serial_enabled\0\u{4}\u{2}button_gpio\0\u{3}buzzer_gpio\0\u{3}rebroadcast_mode\0\u{3}node_info_broadcast_secs\0\u{3}double_tap_as_button_press\0\u{3}is_managed\0\u{3}disable_triple_click\0\u{1}tzdef\0\u{3}led_heartbeat_disabled\0\u{3}buzzer_mode\0") public mutating func decodeMessage(decoder: inout D) throws { while let fieldNumber = try decoder.nextFieldNumber() { @@ -2331,61 +2223,20 @@ extension Config.DeviceConfig: SwiftProtobuf.Message, SwiftProtobuf._MessageImpl } extension Config.DeviceConfig.Role: SwiftProtobuf._ProtoNameProviding { - public static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ - 0: .same(proto: "CLIENT"), - 1: .same(proto: "CLIENT_MUTE"), - 2: .same(proto: "ROUTER"), - 3: .same(proto: "ROUTER_CLIENT"), - 4: .same(proto: "REPEATER"), - 5: .same(proto: "TRACKER"), - 6: .same(proto: "SENSOR"), - 7: .same(proto: "TAK"), - 8: .same(proto: "CLIENT_HIDDEN"), - 9: .same(proto: "LOST_AND_FOUND"), - 10: .same(proto: "TAK_TRACKER"), - 11: .same(proto: "ROUTER_LATE"), - 12: .same(proto: "CLIENT_BASE"), - ] + public static let _protobuf_nameMap = SwiftProtobuf._NameMap(bytecode: "\0\u{2}\0CLIENT\0\u{1}CLIENT_MUTE\0\u{1}ROUTER\0\u{1}ROUTER_CLIENT\0\u{1}REPEATER\0\u{1}TRACKER\0\u{1}SENSOR\0\u{1}TAK\0\u{1}CLIENT_HIDDEN\0\u{1}LOST_AND_FOUND\0\u{1}TAK_TRACKER\0\u{1}ROUTER_LATE\0\u{1}CLIENT_BASE\0") } extension Config.DeviceConfig.RebroadcastMode: SwiftProtobuf._ProtoNameProviding { - public static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ - 0: .same(proto: "ALL"), - 1: .same(proto: "ALL_SKIP_DECODING"), - 2: .same(proto: "LOCAL_ONLY"), - 3: .same(proto: "KNOWN_ONLY"), - 4: .same(proto: "NONE"), - 5: .same(proto: "CORE_PORTNUMS_ONLY"), - ] + public static let _protobuf_nameMap = SwiftProtobuf._NameMap(bytecode: "\0\u{2}\0ALL\0\u{1}ALL_SKIP_DECODING\0\u{1}LOCAL_ONLY\0\u{1}KNOWN_ONLY\0\u{1}NONE\0\u{1}CORE_PORTNUMS_ONLY\0") } extension Config.DeviceConfig.BuzzerMode: SwiftProtobuf._ProtoNameProviding { - public static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ - 0: .same(proto: "ALL_ENABLED"), - 1: .same(proto: "DISABLED"), - 2: .same(proto: "NOTIFICATIONS_ONLY"), - 3: .same(proto: "SYSTEM_ONLY"), - 4: .same(proto: "DIRECT_MSG_ONLY"), - ] + public static let _protobuf_nameMap = SwiftProtobuf._NameMap(bytecode: "\0\u{2}\0ALL_ENABLED\0\u{1}DISABLED\0\u{1}NOTIFICATIONS_ONLY\0\u{1}SYSTEM_ONLY\0\u{1}DIRECT_MSG_ONLY\0") } extension Config.PositionConfig: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { public static let protoMessageName: String = Config.protoMessageName + ".PositionConfig" - public static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ - 1: .standard(proto: "position_broadcast_secs"), - 2: .standard(proto: "position_broadcast_smart_enabled"), - 3: .standard(proto: "fixed_position"), - 4: .standard(proto: "gps_enabled"), - 5: .standard(proto: "gps_update_interval"), - 6: .standard(proto: "gps_attempt_time"), - 7: .standard(proto: "position_flags"), - 8: .standard(proto: "rx_gpio"), - 9: .standard(proto: "tx_gpio"), - 10: .standard(proto: "broadcast_smart_minimum_distance"), - 11: .standard(proto: "broadcast_smart_minimum_interval_secs"), - 12: .standard(proto: "gps_en_gpio"), - 13: .standard(proto: "gps_mode"), - ] + public static let _protobuf_nameMap = SwiftProtobuf._NameMap(bytecode: "\0\u{3}position_broadcast_secs\0\u{3}position_broadcast_smart_enabled\0\u{3}fixed_position\0\u{3}gps_enabled\0\u{3}gps_update_interval\0\u{3}gps_attempt_time\0\u{3}position_flags\0\u{3}rx_gpio\0\u{3}tx_gpio\0\u{3}broadcast_smart_minimum_distance\0\u{3}broadcast_smart_minimum_interval_secs\0\u{3}gps_en_gpio\0\u{3}gps_mode\0") public mutating func decodeMessage(decoder: inout D) throws { while let fieldNumber = try decoder.nextFieldNumber() { @@ -2474,42 +2325,16 @@ extension Config.PositionConfig: SwiftProtobuf.Message, SwiftProtobuf._MessageIm } extension Config.PositionConfig.PositionFlags: SwiftProtobuf._ProtoNameProviding { - public static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ - 0: .same(proto: "UNSET"), - 1: .same(proto: "ALTITUDE"), - 2: .same(proto: "ALTITUDE_MSL"), - 4: .same(proto: "GEOIDAL_SEPARATION"), - 8: .same(proto: "DOP"), - 16: .same(proto: "HVDOP"), - 32: .same(proto: "SATINVIEW"), - 64: .same(proto: "SEQ_NO"), - 128: .same(proto: "TIMESTAMP"), - 256: .same(proto: "HEADING"), - 512: .same(proto: "SPEED"), - ] + public static let _protobuf_nameMap = SwiftProtobuf._NameMap(bytecode: "\0\u{2}\0UNSET\0\u{1}ALTITUDE\0\u{1}ALTITUDE_MSL\0\u{2}\u{2}GEOIDAL_SEPARATION\0\u{2}\u{4}DOP\0\u{2}\u{8}HVDOP\0\u{2}\u{10}SATINVIEW\0\u{2} SEQ_NO\0\u{2}@\u{1}TIMESTAMP\0\u{2}@\u{2}HEADING\0\u{2}@\u{4}SPEED\0") } extension Config.PositionConfig.GpsMode: SwiftProtobuf._ProtoNameProviding { - public static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ - 0: .same(proto: "DISABLED"), - 1: .same(proto: "ENABLED"), - 2: .same(proto: "NOT_PRESENT"), - ] + public static let _protobuf_nameMap = SwiftProtobuf._NameMap(bytecode: "\0\u{2}\0DISABLED\0\u{1}ENABLED\0\u{1}NOT_PRESENT\0") } extension Config.PowerConfig: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { public static let protoMessageName: String = Config.protoMessageName + ".PowerConfig" - public static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ - 1: .standard(proto: "is_power_saving"), - 2: .standard(proto: "on_battery_shutdown_after_secs"), - 3: .standard(proto: "adc_multiplier_override"), - 4: .standard(proto: "wait_bluetooth_secs"), - 6: .standard(proto: "sds_secs"), - 7: .standard(proto: "ls_secs"), - 8: .standard(proto: "min_wake_secs"), - 9: .standard(proto: "device_battery_ina_address"), - 32: .standard(proto: "powermon_enables"), - ] + public static let _protobuf_nameMap = SwiftProtobuf._NameMap(bytecode: "\0\u{3}is_power_saving\0\u{3}on_battery_shutdown_after_secs\0\u{3}adc_multiplier_override\0\u{3}wait_bluetooth_secs\0\u{4}\u{2}sds_secs\0\u{3}ls_secs\0\u{3}min_wake_secs\0\u{3}device_battery_ina_address\0\u{4}\u{17}powermon_enables\0") public mutating func decodeMessage(decoder: inout D) throws { while let fieldNumber = try decoder.nextFieldNumber() { @@ -2538,7 +2363,7 @@ extension Config.PowerConfig: SwiftProtobuf.Message, SwiftProtobuf._MessageImple if self.onBatteryShutdownAfterSecs != 0 { try visitor.visitSingularUInt32Field(value: self.onBatteryShutdownAfterSecs, fieldNumber: 2) } - if self.adcMultiplierOverride != 0 { + if self.adcMultiplierOverride.bitPattern != 0 { try visitor.visitSingularFloatField(value: self.adcMultiplierOverride, fieldNumber: 3) } if self.waitBluetoothSecs != 0 { @@ -2579,18 +2404,7 @@ extension Config.PowerConfig: SwiftProtobuf.Message, SwiftProtobuf._MessageImple extension Config.NetworkConfig: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { public static let protoMessageName: String = Config.protoMessageName + ".NetworkConfig" - public static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ - 1: .standard(proto: "wifi_enabled"), - 3: .standard(proto: "wifi_ssid"), - 4: .standard(proto: "wifi_psk"), - 5: .standard(proto: "ntp_server"), - 6: .standard(proto: "eth_enabled"), - 7: .standard(proto: "address_mode"), - 8: .standard(proto: "ipv4_config"), - 9: .standard(proto: "rsyslog_server"), - 10: .standard(proto: "enabled_protocols"), - 11: .standard(proto: "ipv6_enabled"), - ] + public static let _protobuf_nameMap = SwiftProtobuf._NameMap(bytecode: "\0\u{3}wifi_enabled\0\u{4}\u{2}wifi_ssid\0\u{3}wifi_psk\0\u{3}ntp_server\0\u{3}eth_enabled\0\u{3}address_mode\0\u{3}ipv4_config\0\u{3}rsyslog_server\0\u{3}enabled_protocols\0\u{3}ipv6_enabled\0") public mutating func decodeMessage(decoder: inout D) throws { while let fieldNumber = try decoder.nextFieldNumber() { @@ -2668,27 +2482,16 @@ extension Config.NetworkConfig: SwiftProtobuf.Message, SwiftProtobuf._MessageImp } extension Config.NetworkConfig.AddressMode: SwiftProtobuf._ProtoNameProviding { - public static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ - 0: .same(proto: "DHCP"), - 1: .same(proto: "STATIC"), - ] + public static let _protobuf_nameMap = SwiftProtobuf._NameMap(bytecode: "\0\u{2}\0DHCP\0\u{1}STATIC\0") } extension Config.NetworkConfig.ProtocolFlags: SwiftProtobuf._ProtoNameProviding { - public static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ - 0: .same(proto: "NO_BROADCAST"), - 1: .same(proto: "UDP_BROADCAST"), - ] + public static let _protobuf_nameMap = SwiftProtobuf._NameMap(bytecode: "\0\u{2}\0NO_BROADCAST\0\u{1}UDP_BROADCAST\0") } extension Config.NetworkConfig.IpV4Config: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { public static let protoMessageName: String = Config.NetworkConfig.protoMessageName + ".IpV4Config" - public static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ - 1: .same(proto: "ip"), - 2: .same(proto: "gateway"), - 3: .same(proto: "subnet"), - 4: .same(proto: "dns"), - ] + public static let _protobuf_nameMap = SwiftProtobuf._NameMap(bytecode: "\0\u{1}ip\0\u{1}gateway\0\u{1}subnet\0\u{1}dns\0") public mutating func decodeMessage(decoder: inout D) throws { while let fieldNumber = try decoder.nextFieldNumber() { @@ -2733,21 +2536,7 @@ extension Config.NetworkConfig.IpV4Config: SwiftProtobuf.Message, SwiftProtobuf. extension Config.DisplayConfig: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { public static let protoMessageName: String = Config.protoMessageName + ".DisplayConfig" - public static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ - 1: .standard(proto: "screen_on_secs"), - 2: .standard(proto: "gps_format"), - 3: .standard(proto: "auto_screen_carousel_secs"), - 4: .standard(proto: "compass_north_top"), - 5: .standard(proto: "flip_screen"), - 6: .same(proto: "units"), - 7: .same(proto: "oled"), - 8: .same(proto: "displaymode"), - 9: .standard(proto: "heading_bold"), - 10: .standard(proto: "wake_on_tap_or_motion"), - 11: .standard(proto: "compass_orientation"), - 12: .standard(proto: "use_12h_clock"), - 13: .standard(proto: "use_long_node_name"), - ] + public static let _protobuf_nameMap = SwiftProtobuf._NameMap(bytecode: "\0\u{3}screen_on_secs\0\u{3}gps_format\0\u{3}auto_screen_carousel_secs\0\u{3}compass_north_top\0\u{3}flip_screen\0\u{1}units\0\u{1}oled\0\u{1}displaymode\0\u{3}heading_bold\0\u{3}wake_on_tap_or_motion\0\u{3}compass_orientation\0\u{3}use_12h_clock\0\u{3}use_long_node_name\0") public mutating func decodeMessage(decoder: inout D) throws { while let fieldNumber = try decoder.nextFieldNumber() { @@ -2836,72 +2625,28 @@ extension Config.DisplayConfig: SwiftProtobuf.Message, SwiftProtobuf._MessageImp } extension Config.DisplayConfig.DeprecatedGpsCoordinateFormat: SwiftProtobuf._ProtoNameProviding { - public static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ - 0: .same(proto: "UNUSED"), - ] + public static let _protobuf_nameMap = SwiftProtobuf._NameMap(bytecode: "\0\u{2}\0UNUSED\0") } extension Config.DisplayConfig.DisplayUnits: SwiftProtobuf._ProtoNameProviding { - public static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ - 0: .same(proto: "METRIC"), - 1: .same(proto: "IMPERIAL"), - ] + public static let _protobuf_nameMap = SwiftProtobuf._NameMap(bytecode: "\0\u{2}\0METRIC\0\u{1}IMPERIAL\0") } extension Config.DisplayConfig.OledType: SwiftProtobuf._ProtoNameProviding { - public static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ - 0: .same(proto: "OLED_AUTO"), - 1: .same(proto: "OLED_SSD1306"), - 2: .same(proto: "OLED_SH1106"), - 3: .same(proto: "OLED_SH1107"), - 4: .same(proto: "OLED_SH1107_128_128"), - ] + public static let _protobuf_nameMap = SwiftProtobuf._NameMap(bytecode: "\0\u{2}\0OLED_AUTO\0\u{1}OLED_SSD1306\0\u{1}OLED_SH1106\0\u{1}OLED_SH1107\0\u{1}OLED_SH1107_128_128\0") } extension Config.DisplayConfig.DisplayMode: SwiftProtobuf._ProtoNameProviding { - public static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ - 0: .same(proto: "DEFAULT"), - 1: .same(proto: "TWOCOLOR"), - 2: .same(proto: "INVERTED"), - 3: .same(proto: "COLOR"), - ] + public static let _protobuf_nameMap = SwiftProtobuf._NameMap(bytecode: "\0\u{2}\0DEFAULT\0\u{1}TWOCOLOR\0\u{1}INVERTED\0\u{1}COLOR\0") } extension Config.DisplayConfig.CompassOrientation: SwiftProtobuf._ProtoNameProviding { - public static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ - 0: .same(proto: "DEGREES_0"), - 1: .same(proto: "DEGREES_90"), - 2: .same(proto: "DEGREES_180"), - 3: .same(proto: "DEGREES_270"), - 4: .same(proto: "DEGREES_0_INVERTED"), - 5: .same(proto: "DEGREES_90_INVERTED"), - 6: .same(proto: "DEGREES_180_INVERTED"), - 7: .same(proto: "DEGREES_270_INVERTED"), - ] + public static let _protobuf_nameMap = SwiftProtobuf._NameMap(bytecode: "\0\u{2}\0DEGREES_0\0\u{1}DEGREES_90\0\u{1}DEGREES_180\0\u{1}DEGREES_270\0\u{1}DEGREES_0_INVERTED\0\u{1}DEGREES_90_INVERTED\0\u{1}DEGREES_180_INVERTED\0\u{1}DEGREES_270_INVERTED\0") } extension Config.LoRaConfig: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { public static let protoMessageName: String = Config.protoMessageName + ".LoRaConfig" - public static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ - 1: .standard(proto: "use_preset"), - 2: .standard(proto: "modem_preset"), - 3: .same(proto: "bandwidth"), - 4: .standard(proto: "spread_factor"), - 5: .standard(proto: "coding_rate"), - 6: .standard(proto: "frequency_offset"), - 7: .same(proto: "region"), - 8: .standard(proto: "hop_limit"), - 9: .standard(proto: "tx_enabled"), - 10: .standard(proto: "tx_power"), - 11: .standard(proto: "channel_num"), - 12: .standard(proto: "override_duty_cycle"), - 13: .standard(proto: "sx126x_rx_boosted_gain"), - 14: .standard(proto: "override_frequency"), - 15: .standard(proto: "pa_fan_disabled"), - 103: .standard(proto: "ignore_incoming"), - 104: .standard(proto: "ignore_mqtt"), - 105: .standard(proto: "config_ok_to_mqtt"), - ] + public static let _protobuf_nameMap = SwiftProtobuf._NameMap(bytecode: "\0\u{3}use_preset\0\u{3}modem_preset\0\u{1}bandwidth\0\u{3}spread_factor\0\u{3}coding_rate\0\u{3}frequency_offset\0\u{1}region\0\u{3}hop_limit\0\u{3}tx_enabled\0\u{3}tx_power\0\u{3}channel_num\0\u{3}override_duty_cycle\0\u{3}sx126x_rx_boosted_gain\0\u{3}override_frequency\0\u{3}pa_fan_disabled\0\u{4}X\u{1}ignore_incoming\0\u{3}ignore_mqtt\0\u{3}config_ok_to_mqtt\0") fileprivate class _StorageClass { var _usePreset: Bool = false @@ -2923,15 +2668,11 @@ extension Config.LoRaConfig: SwiftProtobuf.Message, SwiftProtobuf._MessageImplem var _ignoreMqtt: Bool = false var _configOkToMqtt: Bool = false - #if swift(>=5.10) // 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. // This will force a copy to be made of this reference when the first mutation occurs; // hence, it is safe to mark this as `nonisolated(unsafe)`. static nonisolated(unsafe) let defaultInstance = _StorageClass() - #else - static let defaultInstance = _StorageClass() - #endif private init() {} @@ -3013,7 +2754,7 @@ extension Config.LoRaConfig: SwiftProtobuf.Message, SwiftProtobuf._MessageImplem if _storage._codingRate != 0 { try visitor.visitSingularUInt32Field(value: _storage._codingRate, fieldNumber: 5) } - if _storage._frequencyOffset != 0 { + if _storage._frequencyOffset.bitPattern != 0 { try visitor.visitSingularFloatField(value: _storage._frequencyOffset, fieldNumber: 6) } if _storage._region != .unset { @@ -3037,7 +2778,7 @@ extension Config.LoRaConfig: SwiftProtobuf.Message, SwiftProtobuf._MessageImplem if _storage._sx126XRxBoostedGain != false { try visitor.visitSingularBoolField(value: _storage._sx126XRxBoostedGain, fieldNumber: 13) } - if _storage._overrideFrequency != 0 { + if _storage._overrideFrequency.bitPattern != 0 { try visitor.visitSingularFloatField(value: _storage._overrideFrequency, fieldNumber: 14) } if _storage._paFanDisabled != false { @@ -3089,58 +2830,16 @@ extension Config.LoRaConfig: SwiftProtobuf.Message, SwiftProtobuf._MessageImplem } extension Config.LoRaConfig.RegionCode: SwiftProtobuf._ProtoNameProviding { - public static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ - 0: .same(proto: "UNSET"), - 1: .same(proto: "US"), - 2: .same(proto: "EU_433"), - 3: .same(proto: "EU_868"), - 4: .same(proto: "CN"), - 5: .same(proto: "JP"), - 6: .same(proto: "ANZ"), - 7: .same(proto: "KR"), - 8: .same(proto: "TW"), - 9: .same(proto: "RU"), - 10: .same(proto: "IN"), - 11: .same(proto: "NZ_865"), - 12: .same(proto: "TH"), - 13: .same(proto: "LORA_24"), - 14: .same(proto: "UA_433"), - 15: .same(proto: "UA_868"), - 16: .same(proto: "MY_433"), - 17: .same(proto: "MY_919"), - 18: .same(proto: "SG_923"), - 19: .same(proto: "PH_433"), - 20: .same(proto: "PH_868"), - 21: .same(proto: "PH_915"), - 22: .same(proto: "ANZ_433"), - 23: .same(proto: "KZ_433"), - 24: .same(proto: "KZ_863"), - 25: .same(proto: "NP_865"), - 26: .same(proto: "BR_902"), - ] + public static let _protobuf_nameMap = SwiftProtobuf._NameMap(bytecode: "\0\u{2}\0UNSET\0\u{1}US\0\u{1}EU_433\0\u{1}EU_868\0\u{1}CN\0\u{1}JP\0\u{1}ANZ\0\u{1}KR\0\u{1}TW\0\u{1}RU\0\u{1}IN\0\u{1}NZ_865\0\u{1}TH\0\u{1}LORA_24\0\u{1}UA_433\0\u{1}UA_868\0\u{1}MY_433\0\u{1}MY_919\0\u{1}SG_923\0\u{1}PH_433\0\u{1}PH_868\0\u{1}PH_915\0\u{1}ANZ_433\0\u{1}KZ_433\0\u{1}KZ_863\0\u{1}NP_865\0\u{1}BR_902\0") } extension Config.LoRaConfig.ModemPreset: SwiftProtobuf._ProtoNameProviding { - public static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ - 0: .same(proto: "LONG_FAST"), - 1: .same(proto: "LONG_SLOW"), - 2: .same(proto: "VERY_LONG_SLOW"), - 3: .same(proto: "MEDIUM_SLOW"), - 4: .same(proto: "MEDIUM_FAST"), - 5: .same(proto: "SHORT_SLOW"), - 6: .same(proto: "SHORT_FAST"), - 7: .same(proto: "LONG_MODERATE"), - 8: .same(proto: "SHORT_TURBO"), - ] + public static let _protobuf_nameMap = SwiftProtobuf._NameMap(bytecode: "\0\u{2}\0LONG_FAST\0\u{1}LONG_SLOW\0\u{1}VERY_LONG_SLOW\0\u{1}MEDIUM_SLOW\0\u{1}MEDIUM_FAST\0\u{1}SHORT_SLOW\0\u{1}SHORT_FAST\0\u{1}LONG_MODERATE\0\u{1}SHORT_TURBO\0\u{1}LONG_TURBO\0") } extension Config.BluetoothConfig: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { public static let protoMessageName: String = Config.protoMessageName + ".BluetoothConfig" - public static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ - 1: .same(proto: "enabled"), - 2: .same(proto: "mode"), - 3: .standard(proto: "fixed_pin"), - ] + public static let _protobuf_nameMap = SwiftProtobuf._NameMap(bytecode: "\0\u{1}enabled\0\u{1}mode\0\u{3}fixed_pin\0") public mutating func decodeMessage(decoder: inout D) throws { while let fieldNumber = try decoder.nextFieldNumber() { @@ -3179,24 +2878,12 @@ extension Config.BluetoothConfig: SwiftProtobuf.Message, SwiftProtobuf._MessageI } extension Config.BluetoothConfig.PairingMode: SwiftProtobuf._ProtoNameProviding { - public static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ - 0: .same(proto: "RANDOM_PIN"), - 1: .same(proto: "FIXED_PIN"), - 2: .same(proto: "NO_PIN"), - ] + public static let _protobuf_nameMap = SwiftProtobuf._NameMap(bytecode: "\0\u{2}\0RANDOM_PIN\0\u{1}FIXED_PIN\0\u{1}NO_PIN\0") } extension Config.SecurityConfig: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { public static let protoMessageName: String = Config.protoMessageName + ".SecurityConfig" - public static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ - 1: .standard(proto: "public_key"), - 2: .standard(proto: "private_key"), - 3: .standard(proto: "admin_key"), - 4: .standard(proto: "is_managed"), - 5: .standard(proto: "serial_enabled"), - 6: .standard(proto: "debug_log_api_enabled"), - 8: .standard(proto: "admin_channel_enabled"), - ] + public static let _protobuf_nameMap = SwiftProtobuf._NameMap(bytecode: "\0\u{3}public_key\0\u{3}private_key\0\u{3}admin_key\0\u{3}is_managed\0\u{3}serial_enabled\0\u{3}debug_log_api_enabled\0\u{4}\u{2}admin_channel_enabled\0") public mutating func decodeMessage(decoder: inout D) throws { while let fieldNumber = try decoder.nextFieldNumber() { @@ -3259,8 +2946,8 @@ extension Config.SessionkeyConfig: SwiftProtobuf.Message, SwiftProtobuf._Message public static let _protobuf_nameMap = SwiftProtobuf._NameMap() public mutating func decodeMessage(decoder: inout D) throws { - while let _ = try decoder.nextFieldNumber() { - } + // Load everything into unknown fields + while try decoder.nextFieldNumber() != nil {} } public func traverse(visitor: inout V) throws { diff --git a/MeshtasticProtobufs/Sources/meshtastic/connection_status.pb.swift b/MeshtasticProtobufs/Sources/meshtastic/connection_status.pb.swift index a2ec180e..d1f1d208 100644 --- a/MeshtasticProtobufs/Sources/meshtastic/connection_status.pb.swift +++ b/MeshtasticProtobufs/Sources/meshtastic/connection_status.pb.swift @@ -1,5 +1,6 @@ // DO NOT EDIT. // swift-format-ignore-file +// swiftlint:disable all // // Generated by the Swift generator plugin for the protocol buffer compiler. // Source: meshtastic/connection_status.proto @@ -7,7 +8,6 @@ // For information on using the generated types, please see the documentation: // https://github.com/apple/swift-protobuf/ -import Foundation import SwiftProtobuf // If the compiler emits an error on this type, it is because this file @@ -20,7 +20,7 @@ fileprivate struct _GeneratedWithProtocGenSwiftVersion: SwiftProtobuf.ProtobufAP typealias Version = _2 } -public struct DeviceConnectionStatus { +public struct DeviceConnectionStatus: 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. @@ -81,7 +81,7 @@ public struct DeviceConnectionStatus { /// /// WiFi connection status -public struct WifiConnectionStatus { +public struct WifiConnectionStatus: 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. @@ -114,7 +114,7 @@ public struct WifiConnectionStatus { /// /// Ethernet connection status -public struct EthernetConnectionStatus { +public struct EthernetConnectionStatus: 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. @@ -139,7 +139,7 @@ public struct EthernetConnectionStatus { /// /// Ethernet or WiFi connection status -public struct NetworkConnectionStatus { +public struct NetworkConnectionStatus: 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. @@ -167,7 +167,7 @@ public struct NetworkConnectionStatus { /// /// Bluetooth connection status -public struct BluetoothConnectionStatus { +public struct BluetoothConnectionStatus: 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. @@ -191,7 +191,7 @@ public struct BluetoothConnectionStatus { /// /// Serial connection status -public struct SerialConnectionStatus { +public struct SerialConnectionStatus: 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. @@ -209,27 +209,13 @@ public struct SerialConnectionStatus { public init() {} } -#if swift(>=5.5) && canImport(_Concurrency) -extension DeviceConnectionStatus: @unchecked Sendable {} -extension WifiConnectionStatus: @unchecked Sendable {} -extension EthernetConnectionStatus: @unchecked Sendable {} -extension NetworkConnectionStatus: @unchecked Sendable {} -extension BluetoothConnectionStatus: @unchecked Sendable {} -extension SerialConnectionStatus: @unchecked Sendable {} -#endif // swift(>=5.5) && canImport(_Concurrency) - // MARK: - Code below here is support for the SwiftProtobuf runtime. fileprivate let _protobuf_package = "meshtastic" extension DeviceConnectionStatus: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { public static let protoMessageName: String = _protobuf_package + ".DeviceConnectionStatus" - public static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ - 1: .same(proto: "wifi"), - 2: .same(proto: "ethernet"), - 3: .same(proto: "bluetooth"), - 4: .same(proto: "serial"), - ] + public static let _protobuf_nameMap = SwiftProtobuf._NameMap(bytecode: "\0\u{1}wifi\0\u{1}ethernet\0\u{1}bluetooth\0\u{1}serial\0") public mutating func decodeMessage(decoder: inout D) throws { while let fieldNumber = try decoder.nextFieldNumber() { @@ -278,11 +264,7 @@ extension DeviceConnectionStatus: SwiftProtobuf.Message, SwiftProtobuf._MessageI extension WifiConnectionStatus: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { public static let protoMessageName: String = _protobuf_package + ".WifiConnectionStatus" - public static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ - 1: .same(proto: "status"), - 2: .same(proto: "ssid"), - 3: .same(proto: "rssi"), - ] + public static let _protobuf_nameMap = SwiftProtobuf._NameMap(bytecode: "\0\u{1}status\0\u{1}ssid\0\u{1}rssi\0") public mutating func decodeMessage(decoder: inout D) throws { while let fieldNumber = try decoder.nextFieldNumber() { @@ -326,9 +308,7 @@ extension WifiConnectionStatus: SwiftProtobuf.Message, SwiftProtobuf._MessageImp extension EthernetConnectionStatus: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { public static let protoMessageName: String = _protobuf_package + ".EthernetConnectionStatus" - public static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ - 1: .same(proto: "status"), - ] + public static let _protobuf_nameMap = SwiftProtobuf._NameMap(bytecode: "\0\u{1}status\0") public mutating func decodeMessage(decoder: inout D) throws { while let fieldNumber = try decoder.nextFieldNumber() { @@ -362,12 +342,7 @@ extension EthernetConnectionStatus: SwiftProtobuf.Message, SwiftProtobuf._Messag extension NetworkConnectionStatus: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { public static let protoMessageName: String = _protobuf_package + ".NetworkConnectionStatus" - public static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ - 1: .standard(proto: "ip_address"), - 2: .standard(proto: "is_connected"), - 3: .standard(proto: "is_mqtt_connected"), - 4: .standard(proto: "is_syslog_connected"), - ] + public static let _protobuf_nameMap = SwiftProtobuf._NameMap(bytecode: "\0\u{3}ip_address\0\u{3}is_connected\0\u{3}is_mqtt_connected\0\u{3}is_syslog_connected\0") public mutating func decodeMessage(decoder: inout D) throws { while let fieldNumber = try decoder.nextFieldNumber() { @@ -412,11 +387,7 @@ extension NetworkConnectionStatus: SwiftProtobuf.Message, SwiftProtobuf._Message extension BluetoothConnectionStatus: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { public static let protoMessageName: String = _protobuf_package + ".BluetoothConnectionStatus" - public static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ - 1: .same(proto: "pin"), - 2: .same(proto: "rssi"), - 3: .standard(proto: "is_connected"), - ] + public static let _protobuf_nameMap = SwiftProtobuf._NameMap(bytecode: "\0\u{1}pin\0\u{1}rssi\0\u{3}is_connected\0") public mutating func decodeMessage(decoder: inout D) throws { while let fieldNumber = try decoder.nextFieldNumber() { @@ -456,10 +427,7 @@ extension BluetoothConnectionStatus: SwiftProtobuf.Message, SwiftProtobuf._Messa extension SerialConnectionStatus: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { public static let protoMessageName: String = _protobuf_package + ".SerialConnectionStatus" - public static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ - 1: .same(proto: "baud"), - 2: .standard(proto: "is_connected"), - ] + public static let _protobuf_nameMap = SwiftProtobuf._NameMap(bytecode: "\0\u{1}baud\0\u{3}is_connected\0") public mutating func decodeMessage(decoder: inout D) throws { while let fieldNumber = try decoder.nextFieldNumber() { diff --git a/MeshtasticProtobufs/Sources/meshtastic/device_ui.pb.swift b/MeshtasticProtobufs/Sources/meshtastic/device_ui.pb.swift index 4f049e79..327e356d 100644 --- a/MeshtasticProtobufs/Sources/meshtastic/device_ui.pb.swift +++ b/MeshtasticProtobufs/Sources/meshtastic/device_ui.pb.swift @@ -1,5 +1,6 @@ // DO NOT EDIT. // swift-format-ignore-file +// swiftlint:disable all // // Generated by the Swift generator plugin for the protocol buffer compiler. // Source: meshtastic/device_ui.proto @@ -20,7 +21,7 @@ fileprivate struct _GeneratedWithProtocGenSwiftVersion: SwiftProtobuf.ProtobufAP typealias Version = _2 } -public enum CompassMode: SwiftProtobuf.Enum { +public enum CompassMode: SwiftProtobuf.Enum, Swift.CaseIterable { public typealias RawValue = Int /// @@ -58,22 +59,16 @@ public enum CompassMode: SwiftProtobuf.Enum { } } -} - -#if swift(>=4.2) - -extension CompassMode: CaseIterable { // The compiler won't synthesize support with the UNRECOGNIZED case. public static let allCases: [CompassMode] = [ .dynamic, .fixedRing, .freezeHeading, ] + } -#endif // swift(>=4.2) - -public enum Theme: SwiftProtobuf.Enum { +public enum Theme: SwiftProtobuf.Enum, Swift.CaseIterable { public typealias RawValue = Int /// @@ -111,24 +106,18 @@ public enum Theme: SwiftProtobuf.Enum { } } -} - -#if swift(>=4.2) - -extension Theme: CaseIterable { // The compiler won't synthesize support with the UNRECOGNIZED case. public static let allCases: [Theme] = [ .dark, .light, .red, ] -} -#endif // swift(>=4.2) +} /// /// Localization -public enum Language: SwiftProtobuf.Enum { +public enum Language: SwiftProtobuf.Enum, Swift.CaseIterable { public typealias RawValue = Int /// @@ -280,11 +269,6 @@ public enum Language: SwiftProtobuf.Enum { } } -} - -#if swift(>=4.2) - -extension Language: CaseIterable { // The compiler won't synthesize support with the UNRECOGNIZED case. public static let allCases: [Language] = [ .english, @@ -310,11 +294,10 @@ extension Language: CaseIterable { .simplifiedChinese, .traditionalChinese, ] + } -#endif // swift(>=4.2) - -public struct DeviceUIConfig { +public struct DeviceUIConfig: @unchecked 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. @@ -462,7 +445,7 @@ public struct DeviceUIConfig { /// /// How the GPS coordinates are displayed on the OLED screen. - public enum GpsCoordinateFormat: SwiftProtobuf.Enum { + public enum GpsCoordinateFormat: SwiftProtobuf.Enum, Swift.CaseIterable { public typealias RawValue = Int /// @@ -532,6 +515,17 @@ public struct DeviceUIConfig { } } + // The compiler won't synthesize support with the UNRECOGNIZED case. + public static let allCases: [DeviceUIConfig.GpsCoordinateFormat] = [ + .dec, + .dms, + .utm, + .mgrs, + .olc, + .osgr, + .mls, + ] + } public init() {} @@ -539,24 +533,7 @@ public struct DeviceUIConfig { fileprivate var _storage = _StorageClass.defaultInstance } -#if swift(>=4.2) - -extension DeviceUIConfig.GpsCoordinateFormat: CaseIterable { - // The compiler won't synthesize support with the UNRECOGNIZED case. - public static let allCases: [DeviceUIConfig.GpsCoordinateFormat] = [ - .dec, - .dms, - .utm, - .mgrs, - .olc, - .osgr, - .mls, - ] -} - -#endif // swift(>=4.2) - -public struct NodeFilter { +public struct NodeFilter: 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. @@ -594,7 +571,7 @@ public struct NodeFilter { public init() {} } -public struct NodeHighlight { +public struct NodeHighlight: 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. @@ -624,7 +601,7 @@ public struct NodeHighlight { public init() {} } -public struct GeoPoint { +public struct GeoPoint: 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. @@ -646,7 +623,7 @@ public struct GeoPoint { public init() {} } -public struct Map { +public struct Map: 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. @@ -677,88 +654,25 @@ public struct Map { fileprivate var _home: GeoPoint? = nil } -#if swift(>=5.5) && canImport(_Concurrency) -extension CompassMode: @unchecked Sendable {} -extension Theme: @unchecked Sendable {} -extension Language: @unchecked Sendable {} -extension DeviceUIConfig: @unchecked Sendable {} -extension DeviceUIConfig.GpsCoordinateFormat: @unchecked Sendable {} -extension NodeFilter: @unchecked Sendable {} -extension NodeHighlight: @unchecked Sendable {} -extension GeoPoint: @unchecked Sendable {} -extension Map: @unchecked Sendable {} -#endif // swift(>=5.5) && canImport(_Concurrency) - // MARK: - Code below here is support for the SwiftProtobuf runtime. fileprivate let _protobuf_package = "meshtastic" extension CompassMode: SwiftProtobuf._ProtoNameProviding { - public static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ - 0: .same(proto: "DYNAMIC"), - 1: .same(proto: "FIXED_RING"), - 2: .same(proto: "FREEZE_HEADING"), - ] + public static let _protobuf_nameMap = SwiftProtobuf._NameMap(bytecode: "\0\u{2}\0DYNAMIC\0\u{1}FIXED_RING\0\u{1}FREEZE_HEADING\0") } extension Theme: SwiftProtobuf._ProtoNameProviding { - public static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ - 0: .same(proto: "DARK"), - 1: .same(proto: "LIGHT"), - 2: .same(proto: "RED"), - ] + public static let _protobuf_nameMap = SwiftProtobuf._NameMap(bytecode: "\0\u{2}\0DARK\0\u{1}LIGHT\0\u{1}RED\0") } extension Language: SwiftProtobuf._ProtoNameProviding { - public static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ - 0: .same(proto: "ENGLISH"), - 1: .same(proto: "FRENCH"), - 2: .same(proto: "GERMAN"), - 3: .same(proto: "ITALIAN"), - 4: .same(proto: "PORTUGUESE"), - 5: .same(proto: "SPANISH"), - 6: .same(proto: "SWEDISH"), - 7: .same(proto: "FINNISH"), - 8: .same(proto: "POLISH"), - 9: .same(proto: "TURKISH"), - 10: .same(proto: "SERBIAN"), - 11: .same(proto: "RUSSIAN"), - 12: .same(proto: "DUTCH"), - 13: .same(proto: "GREEK"), - 14: .same(proto: "NORWEGIAN"), - 15: .same(proto: "SLOVENIAN"), - 16: .same(proto: "UKRAINIAN"), - 17: .same(proto: "BULGARIAN"), - 18: .same(proto: "CZECH"), - 19: .same(proto: "DANISH"), - 30: .same(proto: "SIMPLIFIED_CHINESE"), - 31: .same(proto: "TRADITIONAL_CHINESE"), - ] + public static let _protobuf_nameMap = SwiftProtobuf._NameMap(bytecode: "\0\u{2}\0ENGLISH\0\u{1}FRENCH\0\u{1}GERMAN\0\u{1}ITALIAN\0\u{1}PORTUGUESE\0\u{1}SPANISH\0\u{1}SWEDISH\0\u{1}FINNISH\0\u{1}POLISH\0\u{1}TURKISH\0\u{1}SERBIAN\0\u{1}RUSSIAN\0\u{1}DUTCH\0\u{1}GREEK\0\u{1}NORWEGIAN\0\u{1}SLOVENIAN\0\u{1}UKRAINIAN\0\u{1}BULGARIAN\0\u{1}CZECH\0\u{1}DANISH\0\u{2}\u{b}SIMPLIFIED_CHINESE\0\u{1}TRADITIONAL_CHINESE\0") } extension DeviceUIConfig: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { public static let protoMessageName: String = _protobuf_package + ".DeviceUIConfig" - public static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ - 1: .same(proto: "version"), - 2: .standard(proto: "screen_brightness"), - 3: .standard(proto: "screen_timeout"), - 4: .standard(proto: "screen_lock"), - 5: .standard(proto: "settings_lock"), - 6: .standard(proto: "pin_code"), - 7: .same(proto: "theme"), - 8: .standard(proto: "alert_enabled"), - 9: .standard(proto: "banner_enabled"), - 10: .standard(proto: "ring_tone_id"), - 11: .same(proto: "language"), - 12: .standard(proto: "node_filter"), - 13: .standard(proto: "node_highlight"), - 14: .standard(proto: "calibration_data"), - 15: .standard(proto: "map_data"), - 16: .standard(proto: "compass_mode"), - 17: .standard(proto: "screen_rgb_color"), - 18: .standard(proto: "is_clockface_analog"), - 19: .standard(proto: "gps_format"), - ] + public static let _protobuf_nameMap = SwiftProtobuf._NameMap(bytecode: "\0\u{1}version\0\u{3}screen_brightness\0\u{3}screen_timeout\0\u{3}screen_lock\0\u{3}settings_lock\0\u{3}pin_code\0\u{1}theme\0\u{3}alert_enabled\0\u{3}banner_enabled\0\u{3}ring_tone_id\0\u{1}language\0\u{3}node_filter\0\u{3}node_highlight\0\u{3}calibration_data\0\u{3}map_data\0\u{3}compass_mode\0\u{3}screen_rgb_color\0\u{3}is_clockface_analog\0\u{3}gps_format\0") fileprivate class _StorageClass { var _version: UInt32 = 0 @@ -781,15 +695,11 @@ extension DeviceUIConfig: SwiftProtobuf.Message, SwiftProtobuf._MessageImplement var _isClockfaceAnalog: Bool = false var _gpsFormat: DeviceUIConfig.GpsCoordinateFormat = .dec - #if swift(>=5.10) // 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. // This will force a copy to be made of this reference when the first mutation occurs; // hence, it is safe to mark this as `nonisolated(unsafe)`. static nonisolated(unsafe) let defaultInstance = _StorageClass() - #else - static let defaultInstance = _StorageClass() - #endif private init() {} @@ -957,28 +867,12 @@ extension DeviceUIConfig: SwiftProtobuf.Message, SwiftProtobuf._MessageImplement } extension DeviceUIConfig.GpsCoordinateFormat: SwiftProtobuf._ProtoNameProviding { - public static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ - 0: .same(proto: "DEC"), - 1: .same(proto: "DMS"), - 2: .same(proto: "UTM"), - 3: .same(proto: "MGRS"), - 4: .same(proto: "OLC"), - 5: .same(proto: "OSGR"), - 6: .same(proto: "MLS"), - ] + public static let _protobuf_nameMap = SwiftProtobuf._NameMap(bytecode: "\0\u{2}\0DEC\0\u{1}DMS\0\u{1}UTM\0\u{1}MGRS\0\u{1}OLC\0\u{1}OSGR\0\u{1}MLS\0") } extension NodeFilter: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { public static let protoMessageName: String = _protobuf_package + ".NodeFilter" - public static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ - 1: .standard(proto: "unknown_switch"), - 2: .standard(proto: "offline_switch"), - 3: .standard(proto: "public_key_switch"), - 4: .standard(proto: "hops_away"), - 5: .standard(proto: "position_switch"), - 6: .standard(proto: "node_name"), - 7: .same(proto: "channel"), - ] + public static let _protobuf_nameMap = SwiftProtobuf._NameMap(bytecode: "\0\u{3}unknown_switch\0\u{3}offline_switch\0\u{3}public_key_switch\0\u{3}hops_away\0\u{3}position_switch\0\u{3}node_name\0\u{1}channel\0") public mutating func decodeMessage(decoder: inout D) throws { while let fieldNumber = try decoder.nextFieldNumber() { @@ -1038,13 +932,7 @@ extension NodeFilter: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementatio extension NodeHighlight: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { public static let protoMessageName: String = _protobuf_package + ".NodeHighlight" - public static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ - 1: .standard(proto: "chat_switch"), - 2: .standard(proto: "position_switch"), - 3: .standard(proto: "telemetry_switch"), - 4: .standard(proto: "iaq_switch"), - 5: .standard(proto: "node_name"), - ] + public static let _protobuf_nameMap = SwiftProtobuf._NameMap(bytecode: "\0\u{3}chat_switch\0\u{3}position_switch\0\u{3}telemetry_switch\0\u{3}iaq_switch\0\u{3}node_name\0") public mutating func decodeMessage(decoder: inout D) throws { while let fieldNumber = try decoder.nextFieldNumber() { @@ -1094,11 +982,7 @@ extension NodeHighlight: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementa extension GeoPoint: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { public static let protoMessageName: String = _protobuf_package + ".GeoPoint" - public static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ - 1: .same(proto: "zoom"), - 2: .same(proto: "latitude"), - 3: .same(proto: "longitude"), - ] + public static let _protobuf_nameMap = SwiftProtobuf._NameMap(bytecode: "\0\u{1}zoom\0\u{1}latitude\0\u{1}longitude\0") public mutating func decodeMessage(decoder: inout D) throws { while let fieldNumber = try decoder.nextFieldNumber() { @@ -1138,11 +1022,7 @@ extension GeoPoint: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationB extension Map: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { public static let protoMessageName: String = _protobuf_package + ".Map" - public static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ - 1: .same(proto: "home"), - 2: .same(proto: "style"), - 3: .standard(proto: "follow_gps"), - ] + public static let _protobuf_nameMap = SwiftProtobuf._NameMap(bytecode: "\0\u{1}home\0\u{1}style\0\u{3}follow_gps\0") public mutating func decodeMessage(decoder: inout D) throws { while let fieldNumber = try decoder.nextFieldNumber() { diff --git a/MeshtasticProtobufs/Sources/meshtastic/deviceonly.pb.swift b/MeshtasticProtobufs/Sources/meshtastic/deviceonly.pb.swift index 9c8d5ad8..281f18d8 100644 --- a/MeshtasticProtobufs/Sources/meshtastic/deviceonly.pb.swift +++ b/MeshtasticProtobufs/Sources/meshtastic/deviceonly.pb.swift @@ -1,5 +1,6 @@ // DO NOT EDIT. // swift-format-ignore-file +// swiftlint:disable all // // Generated by the Swift generator plugin for the protocol buffer compiler. // Source: meshtastic/deviceonly.proto @@ -22,7 +23,7 @@ fileprivate struct _GeneratedWithProtocGenSwiftVersion: SwiftProtobuf.ProtobufAP /// /// Position with static location information only for NodeDBLite -public struct PositionLite { +public struct PositionLite: 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. @@ -57,13 +58,15 @@ public struct PositionLite { public init() {} } -public struct UserLite { +public struct UserLite: 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. /// /// This is the addr of the radio. + /// + /// NOTE: This field was marked as deprecated in the .proto file. public var macaddr: Data = Data() /// @@ -115,7 +118,7 @@ public struct UserLite { fileprivate var _isUnmessagable: Bool? = nil } -public struct NodeInfoLite { +public struct NodeInfoLite: @unchecked 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. @@ -245,7 +248,7 @@ public struct NodeInfoLite { /// FIXME, since we write this each time we enter deep sleep (and have infinite /// flash) it would be better to use some sort of append only data structure for /// the receive queue and use the preferences store for the other stuff -public struct DeviceState { +public struct DeviceState: @unchecked 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. @@ -305,6 +308,8 @@ public struct DeviceState { /// Used only during development. /// Indicates developer is testing and changes should never be saved to flash. /// Deprecated in 2.3.1 + /// + /// NOTE: This field was marked as deprecated in the .proto file. public var noSave: Bool { get {return _storage._noSave} set {_uniqueStorage()._noSave = newValue} @@ -313,6 +318,8 @@ public struct DeviceState { /// /// Previously used to manage GPS factory resets. /// Deprecated in 2.5.23 + /// + /// NOTE: This field was marked as deprecated in the .proto file. public var didGpsReset: Bool { get {return _storage._didGpsReset} set {_uniqueStorage()._didGpsReset = newValue} @@ -345,7 +352,7 @@ public struct DeviceState { fileprivate var _storage = _StorageClass.defaultInstance } -public struct NodeDatabase { +public struct NodeDatabase: 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. @@ -367,7 +374,7 @@ public struct NodeDatabase { /// /// The on-disk saved channels -public struct ChannelFile { +public struct ChannelFile: 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. @@ -389,7 +396,7 @@ public struct ChannelFile { /// /// The on-disk backup of the node's preferences -public struct BackupPreferences { +public struct BackupPreferences: 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. @@ -456,29 +463,13 @@ public struct BackupPreferences { fileprivate var _owner: User? = nil } -#if swift(>=5.5) && canImport(_Concurrency) -extension PositionLite: @unchecked Sendable {} -extension UserLite: @unchecked Sendable {} -extension NodeInfoLite: @unchecked Sendable {} -extension DeviceState: @unchecked Sendable {} -extension NodeDatabase: @unchecked Sendable {} -extension ChannelFile: @unchecked Sendable {} -extension BackupPreferences: @unchecked Sendable {} -#endif // swift(>=5.5) && canImport(_Concurrency) - // MARK: - Code below here is support for the SwiftProtobuf runtime. fileprivate let _protobuf_package = "meshtastic" extension PositionLite: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { public static let protoMessageName: String = _protobuf_package + ".PositionLite" - public static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ - 1: .standard(proto: "latitude_i"), - 2: .standard(proto: "longitude_i"), - 3: .same(proto: "altitude"), - 4: .same(proto: "time"), - 5: .standard(proto: "location_source"), - ] + public static let _protobuf_nameMap = SwiftProtobuf._NameMap(bytecode: "\0\u{3}latitude_i\0\u{3}longitude_i\0\u{1}altitude\0\u{1}time\0\u{3}location_source\0") public mutating func decodeMessage(decoder: inout D) throws { while let fieldNumber = try decoder.nextFieldNumber() { @@ -528,16 +519,7 @@ extension PositionLite: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementat extension UserLite: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { public static let protoMessageName: String = _protobuf_package + ".UserLite" - public static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ - 1: .same(proto: "macaddr"), - 2: .standard(proto: "long_name"), - 3: .standard(proto: "short_name"), - 4: .standard(proto: "hw_model"), - 5: .standard(proto: "is_licensed"), - 6: .same(proto: "role"), - 7: .standard(proto: "public_key"), - 9: .standard(proto: "is_unmessagable"), - ] + public static let _protobuf_nameMap = SwiftProtobuf._NameMap(bytecode: "\0\u{1}macaddr\0\u{3}long_name\0\u{3}short_name\0\u{3}hw_model\0\u{3}is_licensed\0\u{1}role\0\u{3}public_key\0\u{4}\u{2}is_unmessagable\0") public mutating func decodeMessage(decoder: inout D) throws { while let fieldNumber = try decoder.nextFieldNumber() { @@ -606,21 +588,7 @@ extension UserLite: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationB extension NodeInfoLite: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { public static let protoMessageName: String = _protobuf_package + ".NodeInfoLite" - public static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ - 1: .same(proto: "num"), - 2: .same(proto: "user"), - 3: .same(proto: "position"), - 4: .same(proto: "snr"), - 5: .standard(proto: "last_heard"), - 6: .standard(proto: "device_metrics"), - 7: .same(proto: "channel"), - 8: .standard(proto: "via_mqtt"), - 9: .standard(proto: "hops_away"), - 10: .standard(proto: "is_favorite"), - 11: .standard(proto: "is_ignored"), - 12: .standard(proto: "next_hop"), - 13: .same(proto: "bitfield"), - ] + 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}next_hop\0\u{1}bitfield\0") fileprivate class _StorageClass { var _num: UInt32 = 0 @@ -637,15 +605,11 @@ extension NodeInfoLite: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementat var _nextHop: UInt32 = 0 var _bitfield: UInt32 = 0 - #if swift(>=5.10) // 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. // This will force a copy to be made of this reference when the first mutation occurs; // hence, it is safe to mark this as `nonisolated(unsafe)`. static nonisolated(unsafe) let defaultInstance = _StorageClass() - #else - static let defaultInstance = _StorageClass() - #endif private init() {} @@ -715,7 +679,7 @@ extension NodeInfoLite: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementat try { if let v = _storage._position { try visitor.visitSingularMessageField(value: v, fieldNumber: 3) } }() - if _storage._snr != 0 { + if _storage._snr.bitPattern != 0 { try visitor.visitSingularFloatField(value: _storage._snr, fieldNumber: 4) } if _storage._lastHeard != 0 { @@ -778,17 +742,7 @@ extension NodeInfoLite: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementat extension DeviceState: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { public static let protoMessageName: String = _protobuf_package + ".DeviceState" - public static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ - 2: .standard(proto: "my_node"), - 3: .same(proto: "owner"), - 5: .standard(proto: "receive_queue"), - 8: .same(proto: "version"), - 7: .standard(proto: "rx_text_message"), - 9: .standard(proto: "no_save"), - 11: .standard(proto: "did_gps_reset"), - 12: .standard(proto: "rx_waypoint"), - 13: .standard(proto: "node_remote_hardware_pins"), - ] + public static let _protobuf_nameMap = SwiftProtobuf._NameMap(bytecode: "\0\u{4}\u{2}my_node\0\u{1}owner\0\u{4}\u{2}receive_queue\0\u{4}\u{2}rx_text_message\0\u{1}version\0\u{3}no_save\0\u{4}\u{2}did_gps_reset\0\u{3}rx_waypoint\0\u{3}node_remote_hardware_pins\0") fileprivate class _StorageClass { var _myNode: MyNodeInfo? = nil @@ -801,15 +755,11 @@ extension DeviceState: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementati var _rxWaypoint: MeshPacket? = nil var _nodeRemoteHardwarePins: [NodeRemoteHardwarePin] = [] - #if swift(>=5.10) // 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. // This will force a copy to be made of this reference when the first mutation occurs; // hence, it is safe to mark this as `nonisolated(unsafe)`. static nonisolated(unsafe) let defaultInstance = _StorageClass() - #else - static let defaultInstance = _StorageClass() - #endif private init() {} @@ -918,10 +868,7 @@ extension DeviceState: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementati extension NodeDatabase: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { public static let protoMessageName: String = _protobuf_package + ".NodeDatabase" - public static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ - 1: .same(proto: "version"), - 2: .same(proto: "nodes"), - ] + public static let _protobuf_nameMap = SwiftProtobuf._NameMap(bytecode: "\0\u{1}version\0\u{1}nodes\0") public mutating func decodeMessage(decoder: inout D) throws { while let fieldNumber = try decoder.nextFieldNumber() { @@ -956,10 +903,7 @@ extension NodeDatabase: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementat extension ChannelFile: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { public static let protoMessageName: String = _protobuf_package + ".ChannelFile" - public static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ - 1: .same(proto: "channels"), - 2: .same(proto: "version"), - ] + public static let _protobuf_nameMap = SwiftProtobuf._NameMap(bytecode: "\0\u{1}channels\0\u{1}version\0") public mutating func decodeMessage(decoder: inout D) throws { while let fieldNumber = try decoder.nextFieldNumber() { @@ -994,14 +938,7 @@ extension ChannelFile: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementati extension BackupPreferences: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { public static let protoMessageName: String = _protobuf_package + ".BackupPreferences" - public static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ - 1: .same(proto: "version"), - 2: .same(proto: "timestamp"), - 3: .same(proto: "config"), - 4: .standard(proto: "module_config"), - 5: .same(proto: "channels"), - 6: .same(proto: "owner"), - ] + public static let _protobuf_nameMap = SwiftProtobuf._NameMap(bytecode: "\0\u{1}version\0\u{1}timestamp\0\u{1}config\0\u{3}module_config\0\u{1}channels\0\u{1}owner\0") public mutating func decodeMessage(decoder: inout D) throws { while let fieldNumber = try decoder.nextFieldNumber() { diff --git a/MeshtasticProtobufs/Sources/meshtastic/interdevice.pb.swift b/MeshtasticProtobufs/Sources/meshtastic/interdevice.pb.swift index 92b72c15..3e28112d 100644 --- a/MeshtasticProtobufs/Sources/meshtastic/interdevice.pb.swift +++ b/MeshtasticProtobufs/Sources/meshtastic/interdevice.pb.swift @@ -1,5 +1,6 @@ // DO NOT EDIT. // swift-format-ignore-file +// swiftlint:disable all // // Generated by the Swift generator plugin for the protocol buffer compiler. // Source: meshtastic/interdevice.proto @@ -7,7 +8,6 @@ // For information on using the generated types, please see the documentation: // https://github.com/apple/swift-protobuf/ -import Foundation import SwiftProtobuf // If the compiler emits an error on this type, it is because this file @@ -20,7 +20,7 @@ fileprivate struct _GeneratedWithProtocGenSwiftVersion: SwiftProtobuf.ProtobufAP typealias Version = _2 } -public enum MessageType: SwiftProtobuf.Enum { +public enum MessageType: SwiftProtobuf.Enum, Swift.CaseIterable { public typealias RawValue = Int case ack // = 0 @@ -82,11 +82,6 @@ public enum MessageType: SwiftProtobuf.Enum { } } -} - -#if swift(>=4.2) - -extension MessageType: CaseIterable { // The compiler won't synthesize support with the UNRECOGNIZED case. public static let allCases: [MessageType] = [ .ack, @@ -102,11 +97,10 @@ extension MessageType: CaseIterable { .aht20Humidity, .tvocIndex, ] + } -#endif // swift(>=4.2) - -public struct SensorData { +public struct SensorData: 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. @@ -136,34 +130,16 @@ public struct SensorData { public var unknownFields = SwiftProtobuf.UnknownStorage() /// The sensor data, either as a float or an uint32 - public enum OneOf_Data: Equatable { + public enum OneOf_Data: Equatable, Sendable { case floatValue(Float) case uint32Value(UInt32) - #if !swift(>=4.1) - public static func ==(lhs: SensorData.OneOf_Data, rhs: SensorData.OneOf_Data) -> Bool { - // 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 (lhs, rhs) { - case (.floatValue, .floatValue): return { - guard case .floatValue(let l) = lhs, case .floatValue(let r) = rhs else { preconditionFailure() } - return l == r - }() - case (.uint32Value, .uint32Value): return { - guard case .uint32Value(let l) = lhs, case .uint32Value(let r) = rhs else { preconditionFailure() } - return l == r - }() - default: return false - } - } - #endif } public init() {} } -public struct InterdeviceMessage { +public struct InterdeviceMessage: 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. @@ -190,69 +166,26 @@ public struct InterdeviceMessage { public var unknownFields = SwiftProtobuf.UnknownStorage() /// The message data - public enum OneOf_Data: Equatable { + public enum OneOf_Data: Equatable, Sendable { case nmea(String) case sensor(SensorData) - #if !swift(>=4.1) - public static func ==(lhs: InterdeviceMessage.OneOf_Data, rhs: InterdeviceMessage.OneOf_Data) -> Bool { - // 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 (lhs, rhs) { - case (.nmea, .nmea): return { - guard case .nmea(let l) = lhs, case .nmea(let r) = rhs else { preconditionFailure() } - return l == r - }() - case (.sensor, .sensor): return { - guard case .sensor(let l) = lhs, case .sensor(let r) = rhs else { preconditionFailure() } - return l == r - }() - default: return false - } - } - #endif } public init() {} } -#if swift(>=5.5) && canImport(_Concurrency) -extension MessageType: @unchecked Sendable {} -extension SensorData: @unchecked Sendable {} -extension SensorData.OneOf_Data: @unchecked Sendable {} -extension InterdeviceMessage: @unchecked Sendable {} -extension InterdeviceMessage.OneOf_Data: @unchecked Sendable {} -#endif // swift(>=5.5) && canImport(_Concurrency) - // MARK: - Code below here is support for the SwiftProtobuf runtime. fileprivate let _protobuf_package = "meshtastic" extension MessageType: SwiftProtobuf._ProtoNameProviding { - public static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ - 0: .same(proto: "ACK"), - 160: .same(proto: "COLLECT_INTERVAL"), - 161: .same(proto: "BEEP_ON"), - 162: .same(proto: "BEEP_OFF"), - 163: .same(proto: "SHUTDOWN"), - 164: .same(proto: "POWER_ON"), - 176: .same(proto: "SCD41_TEMP"), - 177: .same(proto: "SCD41_HUMIDITY"), - 178: .same(proto: "SCD41_CO2"), - 179: .same(proto: "AHT20_TEMP"), - 180: .same(proto: "AHT20_HUMIDITY"), - 181: .same(proto: "TVOC_INDEX"), - ] + public static let _protobuf_nameMap = SwiftProtobuf._NameMap(bytecode: "\0\u{2}\0ACK\0\u{2}`\u{2}COLLECT_INTERVAL\0\u{1}BEEP_ON\0\u{1}BEEP_OFF\0\u{1}SHUTDOWN\0\u{1}POWER_ON\0\u{2}\u{c}SCD41_TEMP\0\u{1}SCD41_HUMIDITY\0\u{1}SCD41_CO2\0\u{1}AHT20_TEMP\0\u{1}AHT20_HUMIDITY\0\u{1}TVOC_INDEX\0") } extension SensorData: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { public static let protoMessageName: String = _protobuf_package + ".SensorData" - public static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ - 1: .same(proto: "type"), - 2: .standard(proto: "float_value"), - 3: .standard(proto: "uint32_value"), - ] + public static let _protobuf_nameMap = SwiftProtobuf._NameMap(bytecode: "\0\u{1}type\0\u{3}float_value\0\u{3}uint32_value\0") public mutating func decodeMessage(decoder: inout D) throws { while let fieldNumber = try decoder.nextFieldNumber() { @@ -314,10 +247,7 @@ extension SensorData: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementatio extension InterdeviceMessage: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { public static let protoMessageName: String = _protobuf_package + ".InterdeviceMessage" - public static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ - 1: .same(proto: "nmea"), - 2: .same(proto: "sensor"), - ] + public static let _protobuf_nameMap = SwiftProtobuf._NameMap(bytecode: "\0\u{1}nmea\0\u{1}sensor\0") public mutating func decodeMessage(decoder: inout D) throws { while let fieldNumber = try decoder.nextFieldNumber() { diff --git a/MeshtasticProtobufs/Sources/meshtastic/localonly.pb.swift b/MeshtasticProtobufs/Sources/meshtastic/localonly.pb.swift index 0af27466..9ba9dd88 100644 --- a/MeshtasticProtobufs/Sources/meshtastic/localonly.pb.swift +++ b/MeshtasticProtobufs/Sources/meshtastic/localonly.pb.swift @@ -1,5 +1,6 @@ // DO NOT EDIT. // swift-format-ignore-file +// swiftlint:disable all // // Generated by the Swift generator plugin for the protocol buffer compiler. // Source: meshtastic/localonly.proto @@ -7,7 +8,6 @@ // For information on using the generated types, please see the documentation: // https://github.com/apple/swift-protobuf/ -import Foundation import SwiftProtobuf // If the compiler emits an error on this type, it is because this file @@ -20,7 +20,7 @@ fileprivate struct _GeneratedWithProtocGenSwiftVersion: SwiftProtobuf.ProtobufAP typealias Version = _2 } -public struct LocalConfig { +public struct LocalConfig: @unchecked 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. @@ -129,7 +129,7 @@ public struct LocalConfig { fileprivate var _storage = _StorageClass.defaultInstance } -public struct LocalModuleConfig { +public struct LocalModuleConfig: @unchecked 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. @@ -293,28 +293,13 @@ public struct LocalModuleConfig { fileprivate var _storage = _StorageClass.defaultInstance } -#if swift(>=5.5) && canImport(_Concurrency) -extension LocalConfig: @unchecked Sendable {} -extension LocalModuleConfig: @unchecked Sendable {} -#endif // swift(>=5.5) && canImport(_Concurrency) - // MARK: - Code below here is support for the SwiftProtobuf runtime. fileprivate let _protobuf_package = "meshtastic" extension LocalConfig: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { public static let protoMessageName: String = _protobuf_package + ".LocalConfig" - public static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ - 1: .same(proto: "device"), - 2: .same(proto: "position"), - 3: .same(proto: "power"), - 4: .same(proto: "network"), - 5: .same(proto: "display"), - 6: .same(proto: "lora"), - 7: .same(proto: "bluetooth"), - 8: .same(proto: "version"), - 9: .same(proto: "security"), - ] + public static let _protobuf_nameMap = SwiftProtobuf._NameMap(bytecode: "\0\u{1}device\0\u{1}position\0\u{1}power\0\u{1}network\0\u{1}display\0\u{1}lora\0\u{1}bluetooth\0\u{1}version\0\u{1}security\0") fileprivate class _StorageClass { var _device: Config.DeviceConfig? = nil @@ -327,15 +312,11 @@ extension LocalConfig: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementati var _version: UInt32 = 0 var _security: Config.SecurityConfig? = nil - #if swift(>=5.10) // 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. // This will force a copy to be made of this reference when the first mutation occurs; // hence, it is safe to mark this as `nonisolated(unsafe)`. static nonisolated(unsafe) let defaultInstance = _StorageClass() - #else - static let defaultInstance = _StorageClass() - #endif private init() {} @@ -444,22 +425,7 @@ extension LocalConfig: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementati extension LocalModuleConfig: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { public static let protoMessageName: String = _protobuf_package + ".LocalModuleConfig" - public static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ - 1: .same(proto: "mqtt"), - 2: .same(proto: "serial"), - 3: .standard(proto: "external_notification"), - 4: .standard(proto: "store_forward"), - 5: .standard(proto: "range_test"), - 6: .same(proto: "telemetry"), - 7: .standard(proto: "canned_message"), - 9: .same(proto: "audio"), - 10: .standard(proto: "remote_hardware"), - 11: .standard(proto: "neighbor_info"), - 12: .standard(proto: "ambient_lighting"), - 13: .standard(proto: "detection_sensor"), - 14: .same(proto: "paxcounter"), - 8: .same(proto: "version"), - ] + public static let _protobuf_nameMap = SwiftProtobuf._NameMap(bytecode: "\0\u{1}mqtt\0\u{1}serial\0\u{3}external_notification\0\u{3}store_forward\0\u{3}range_test\0\u{1}telemetry\0\u{3}canned_message\0\u{1}version\0\u{1}audio\0\u{3}remote_hardware\0\u{3}neighbor_info\0\u{3}ambient_lighting\0\u{3}detection_sensor\0\u{1}paxcounter\0") fileprivate class _StorageClass { var _mqtt: ModuleConfig.MQTTConfig? = nil @@ -477,15 +443,11 @@ extension LocalModuleConfig: SwiftProtobuf.Message, SwiftProtobuf._MessageImplem var _paxcounter: ModuleConfig.PaxcounterConfig? = nil var _version: UInt32 = 0 - #if swift(>=5.10) // 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. // This will force a copy to be made of this reference when the first mutation occurs; // hence, it is safe to mark this as `nonisolated(unsafe)`. static nonisolated(unsafe) let defaultInstance = _StorageClass() - #else - static let defaultInstance = _StorageClass() - #endif private init() {} diff --git a/MeshtasticProtobufs/Sources/meshtastic/mesh.pb.swift b/MeshtasticProtobufs/Sources/meshtastic/mesh.pb.swift index c32dfb49..477e2457 100644 --- a/MeshtasticProtobufs/Sources/meshtastic/mesh.pb.swift +++ b/MeshtasticProtobufs/Sources/meshtastic/mesh.pb.swift @@ -1,5 +1,6 @@ // DO NOT EDIT. // swift-format-ignore-file +// swiftlint:disable all // // Generated by the Swift generator plugin for the protocol buffer compiler. // Source: meshtastic/mesh.proto @@ -25,7 +26,7 @@ fileprivate struct _GeneratedWithProtocGenSwiftVersion: SwiftProtobuf.ProtobufAP /// bin/build-all.sh script. /// Because they will be used to find firmware filenames in the android app for OTA updates. /// To match the old style filenames, _ is converted to -, p is converted to . -public enum HardwareModel: SwiftProtobuf.Enum { +public enum HardwareModel: SwiftProtobuf.Enum, Swift.CaseIterable { public typealias RawValue = Int /// @@ -422,8 +423,8 @@ public enum HardwareModel: SwiftProtobuf.Enum { case heltecSensorHub // = 92 /// - /// Reserved Fried Chicken ID for future use - case reservedFriedChicken // = 93 + /// Muzi Works Muzi-Base device + case muziBase // = 93 /// /// Heltec Magnetic Power Bank with Meshtastic compatible @@ -510,6 +511,30 @@ public enum HardwareModel: SwiftProtobuf.Enum { /// LilyGo T-Watch Ultra case tWatchUltra // = 114 + /// + /// Elecrow ThinkNode M3 + case thinknodeM3 // = 115 + + /// + /// RAK WISMESH_TAP_V2 with ESP32-S3 CPU + case wismeshTapV2 // = 116 + + /// + /// RAK3401 + case rak3401 // = 117 + + /// + /// RAK6421 Hat+ + case rak6421 // = 118 + + /// + /// Elecrow ThinkNode M4 + case thinknodeM4 // = 119 + + /// + /// Elecrow ThinkNode M6 + case thinknodeM6 // = 120 + /// /// ------------------------------------------------------------------------------------------------------------------------------------------ /// 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. @@ -616,7 +641,7 @@ public enum HardwareModel: SwiftProtobuf.Enum { case 90: self = .thinknodeM2 case 91: self = .tEthElite case 92: self = .heltecSensorHub - case 93: self = .reservedFriedChicken + case 93: self = .muziBase case 94: self = .heltecMeshPocket case 95: self = .seeedSolarNode case 96: self = .nomadstarMeteorPro @@ -638,6 +663,12 @@ public enum HardwareModel: SwiftProtobuf.Enum { case 112: self = .m5StackCardputerAdv case 113: self = .heltecWirelessTrackerV2 case 114: self = .tWatchUltra + case 115: self = .thinknodeM3 + case 116: self = .wismeshTapV2 + case 117: self = .rak3401 + case 118: self = .rak6421 + case 119: self = .thinknodeM4 + case 120: self = .thinknodeM6 case 255: self = .privateHw default: self = .UNRECOGNIZED(rawValue) } @@ -738,7 +769,7 @@ public enum HardwareModel: SwiftProtobuf.Enum { case .thinknodeM2: return 90 case .tEthElite: return 91 case .heltecSensorHub: return 92 - case .reservedFriedChicken: return 93 + case .muziBase: return 93 case .heltecMeshPocket: return 94 case .seeedSolarNode: return 95 case .nomadstarMeteorPro: return 96 @@ -760,16 +791,17 @@ public enum HardwareModel: SwiftProtobuf.Enum { case .m5StackCardputerAdv: return 112 case .heltecWirelessTrackerV2: return 113 case .tWatchUltra: return 114 + case .thinknodeM3: return 115 + case .wismeshTapV2: return 116 + case .rak3401: return 117 + case .rak6421: return 118 + case .thinknodeM4: return 119 + case .thinknodeM6: return 120 case .privateHw: return 255 case .UNRECOGNIZED(let i): return i } } -} - -#if swift(>=4.2) - -extension HardwareModel: CaseIterable { // The compiler won't synthesize support with the UNRECOGNIZED case. public static let allCases: [HardwareModel] = [ .unset, @@ -865,7 +897,7 @@ extension HardwareModel: CaseIterable { .thinknodeM2, .tEthElite, .heltecSensorHub, - .reservedFriedChicken, + .muziBase, .heltecMeshPocket, .seeedSolarNode, .nomadstarMeteorPro, @@ -887,15 +919,20 @@ extension HardwareModel: CaseIterable { .m5StackCardputerAdv, .heltecWirelessTrackerV2, .tWatchUltra, + .thinknodeM3, + .wismeshTapV2, + .rak3401, + .rak6421, + .thinknodeM4, + .thinknodeM6, .privateHw, ] -} -#endif // swift(>=4.2) +} /// /// Shared constants between device and phone -public enum Constants: SwiftProtobuf.Enum { +public enum Constants: SwiftProtobuf.Enum, Swift.CaseIterable { public typealias RawValue = Int /// @@ -930,26 +967,20 @@ public enum Constants: SwiftProtobuf.Enum { } } -} - -#if swift(>=4.2) - -extension Constants: CaseIterable { // The compiler won't synthesize support with the UNRECOGNIZED case. public static let allCases: [Constants] = [ .zero, .dataPayloadLen, ] -} -#endif // swift(>=4.2) +} /// /// Error codes for critical errors /// The device might report these fault codes on the screen. /// If you encounter a fault code, please post on the meshtastic.discourse.group /// and we'll try to help. -public enum CriticalErrorCode: SwiftProtobuf.Enum { +public enum CriticalErrorCode: SwiftProtobuf.Enum, Swift.CaseIterable { public typealias RawValue = Int /// @@ -1058,11 +1089,6 @@ public enum CriticalErrorCode: SwiftProtobuf.Enum { } } -} - -#if swift(>=4.2) - -extension CriticalErrorCode: CaseIterable { // The compiler won't synthesize support with the UNRECOGNIZED case. public static let allCases: [CriticalErrorCode] = [ .none, @@ -1080,14 +1106,13 @@ extension CriticalErrorCode: CaseIterable { .flashCorruptionRecoverable, .flashCorruptionUnrecoverable, ] -} -#endif // swift(>=4.2) +} /// /// Enum to indicate to clients whether this firmware is a special firmware build, like an event. /// The first 16 values are reserved for non-event special firmwares, like the Smart Citizen use case. -public enum FirmwareEdition: SwiftProtobuf.Enum { +public enum FirmwareEdition: SwiftProtobuf.Enum, Swift.CaseIterable { public typealias RawValue = Int /// @@ -1149,11 +1174,6 @@ public enum FirmwareEdition: SwiftProtobuf.Enum { } } -} - -#if swift(>=4.2) - -extension FirmwareEdition: CaseIterable { // The compiler won't synthesize support with the UNRECOGNIZED case. public static let allCases: [FirmwareEdition] = [ .vanilla, @@ -1164,15 +1184,14 @@ extension FirmwareEdition: CaseIterable { .hamvention, .diyEdition, ] -} -#endif // swift(>=4.2) +} /// /// Enum for modules excluded from a device's configuration. /// Each value represents a ModuleConfigType that can be toggled as excluded /// by setting its corresponding bit in the `excluded_modules` bitmask field. -public enum ExcludedModules: SwiftProtobuf.Enum { +public enum ExcludedModules: SwiftProtobuf.Enum, Swift.CaseIterable { public typealias RawValue = Int /// @@ -1288,11 +1307,6 @@ public enum ExcludedModules: SwiftProtobuf.Enum { } } -} - -#if swift(>=4.2) - -extension ExcludedModules: CaseIterable { // The compiler won't synthesize support with the UNRECOGNIZED case. public static let allCases: [ExcludedModules] = [ .excludedNone, @@ -1312,13 +1326,12 @@ extension ExcludedModules: CaseIterable { .bluetoothConfig, .networkConfig, ] -} -#endif // swift(>=4.2) +} /// /// A GPS Position -public struct Position { +public struct Position: @unchecked 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. @@ -1535,7 +1548,7 @@ public struct Position { /// /// How the location was acquired: manual, onboard GPS, external (EUD) GPS - public enum LocSource: SwiftProtobuf.Enum { + public enum LocSource: SwiftProtobuf.Enum, Swift.CaseIterable { public typealias RawValue = Int /// @@ -1579,12 +1592,20 @@ public struct Position { } } + // The compiler won't synthesize support with the UNRECOGNIZED case. + public static let allCases: [Position.LocSource] = [ + .locUnset, + .locManual, + .locInternal, + .locExternal, + ] + } /// /// How the altitude was acquired: manual, GPS int/ext, etc /// Default: same as location_source if present - public enum AltSource: SwiftProtobuf.Enum { + public enum AltSource: SwiftProtobuf.Enum, Swift.CaseIterable { public typealias RawValue = Int /// @@ -1634,6 +1655,15 @@ public struct Position { } } + // The compiler won't synthesize support with the UNRECOGNIZED case. + public static let allCases: [Position.AltSource] = [ + .altUnset, + .altManual, + .altInternal, + .altExternal, + .altBarometric, + ] + } public init() {} @@ -1641,31 +1671,6 @@ public struct Position { fileprivate var _storage = _StorageClass.defaultInstance } -#if swift(>=4.2) - -extension Position.LocSource: CaseIterable { - // The compiler won't synthesize support with the UNRECOGNIZED case. - public static let allCases: [Position.LocSource] = [ - .locUnset, - .locManual, - .locInternal, - .locExternal, - ] -} - -extension Position.AltSource: CaseIterable { - // The compiler won't synthesize support with the UNRECOGNIZED case. - public static let allCases: [Position.AltSource] = [ - .altUnset, - .altManual, - .altInternal, - .altExternal, - .altBarometric, - ] -} - -#endif // swift(>=4.2) - /// /// Broadcast when a newly powered mesh node wants to find a node num it can use /// Sent from the phone over bluetooth to set the user id for the owner of this node. @@ -1687,7 +1692,7 @@ extension Position.AltSource: CaseIterable { /// A few nodenums are reserved and will never be requested: /// 0xff - broadcast /// 0 through 3 - for future use -public struct User { +public struct User: 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. @@ -1712,6 +1717,8 @@ public struct User { /// Deprecated in Meshtastic 2.1.x /// This is the addr of the radio. /// Not populated by the phone, but added by the esp32 when broadcasting + /// + /// NOTE: This field was marked as deprecated in the .proto file. public var macaddr: Data = Data() /// @@ -1756,7 +1763,7 @@ public struct User { /// /// A message used in a traceroute -public struct RouteDiscovery { +public struct RouteDiscovery: 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. @@ -1784,7 +1791,7 @@ public struct RouteDiscovery { /// /// A Routing control Data packet handled by the routing module -public struct Routing { +public struct Routing: 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. @@ -1824,7 +1831,7 @@ public struct Routing { public var unknownFields = SwiftProtobuf.UnknownStorage() - public enum OneOf_Variant: Equatable { + public enum OneOf_Variant: Equatable, Sendable { /// /// A route request going from the requester case routeRequest(RouteDiscovery) @@ -1836,34 +1843,12 @@ public struct Routing { /// in addition to ack.fail_id to provide details on the type of failure). case errorReason(Routing.Error) - #if !swift(>=4.1) - public static func ==(lhs: Routing.OneOf_Variant, rhs: Routing.OneOf_Variant) -> Bool { - // 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 (lhs, rhs) { - case (.routeRequest, .routeRequest): return { - guard case .routeRequest(let l) = lhs, case .routeRequest(let r) = rhs else { preconditionFailure() } - return l == r - }() - case (.routeReply, .routeReply): return { - guard case .routeReply(let l) = lhs, case .routeReply(let r) = rhs else { preconditionFailure() } - return l == r - }() - case (.errorReason, .errorReason): return { - guard case .errorReason(let l) = lhs, case .errorReason(let r) = rhs else { preconditionFailure() } - return l == r - }() - default: return false - } - } - #endif } /// /// A failure in delivering a message (usually used for routing control messages, but might be provided in addition to ack.fail_id to provide /// details on the type of failure). - public enum Error: SwiftProtobuf.Enum { + public enum Error: SwiftProtobuf.Enum, Swift.CaseIterable { public typealias RawValue = Int /// @@ -1988,43 +1973,37 @@ public struct Routing { } } + // The compiler won't synthesize support with the UNRECOGNIZED case. + public static let allCases: [Routing.Error] = [ + .none, + .noRoute, + .gotNak, + .timeout, + .noInterface, + .maxRetransmit, + .noChannel, + .tooLarge, + .noResponse, + .dutyCycleLimit, + .badRequest, + .notAuthorized, + .pkiFailed, + .pkiUnknownPubkey, + .adminBadSessionKey, + .adminPublicKeyUnauthorized, + .rateLimitExceeded, + ] + } public init() {} } -#if swift(>=4.2) - -extension Routing.Error: CaseIterable { - // The compiler won't synthesize support with the UNRECOGNIZED case. - public static let allCases: [Routing.Error] = [ - .none, - .noRoute, - .gotNak, - .timeout, - .noInterface, - .maxRetransmit, - .noChannel, - .tooLarge, - .noResponse, - .dutyCycleLimit, - .badRequest, - .notAuthorized, - .pkiFailed, - .pkiUnknownPubkey, - .adminBadSessionKey, - .adminPublicKeyUnauthorized, - .rateLimitExceeded, - ] -} - -#endif // swift(>=4.2) - /// /// (Formerly called SubPacket) /// The payload portion fo a packet, this is the actual bytes that are sent /// inside a radio packet (because from/to are broken out by the comms library) -public struct DataMessage { +public struct DataMessage: 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. @@ -2091,7 +2070,7 @@ public struct DataMessage { /// /// The actual over-the-mesh message doing KeyVerification -public struct KeyVerification { +public struct KeyVerification: 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. @@ -2114,9 +2093,127 @@ public struct KeyVerification { public init() {} } +/// +/// The actual over-the-mesh message doing store and forward++ +public struct StoreForwardPlusPlus: 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. + + /// + /// Which message type is this + public var sfppMessageType: StoreForwardPlusPlus.SFPP_message_type = .canonAnnounce + + /// + /// The hash of the specific message + public var messageHash: Data = Data() + + /// + /// The hash of a link on a chain + public var commitHash: Data = Data() + + /// + /// the root hash of a chain + public var rootHash: Data = Data() + + /// + /// The encrypted bytes from a message + public var message: Data = Data() + + /// + /// Message ID of the contained message + public var encapsulatedID: UInt32 = 0 + + /// + /// Destination of the contained message + public var encapsulatedTo: UInt32 = 0 + + /// + /// Sender of the contained message + public var encapsulatedFrom: UInt32 = 0 + + /// + /// The receive time of the message in question + public var encapsulatedRxtime: UInt32 = 0 + + public var unknownFields = SwiftProtobuf.UnknownStorage() + + /// + /// Enum of message types + public enum SFPP_message_type: SwiftProtobuf.Enum, Swift.CaseIterable { + public typealias RawValue = Int + + /// + /// Send an announcement of the canonical tip of a chain + case canonAnnounce // = 0 + + /// + /// Query whether a specific link is on the chain + case chainQuery // = 1 + + /// + /// Request the next link in the chain + case linkRequest // = 3 + + /// + /// Provide a link to add to the chain + case linkProvide // = 4 + + /// + /// If we must fragment, send the first half + case linkProvideFirsthalf // = 5 + + /// + /// If we must fragment, send the second half + case linkProvideSecondhalf // = 6 + case UNRECOGNIZED(Int) + + public init() { + self = .canonAnnounce + } + + public init?(rawValue: Int) { + switch rawValue { + case 0: self = .canonAnnounce + case 1: self = .chainQuery + case 3: self = .linkRequest + case 4: self = .linkProvide + case 5: self = .linkProvideFirsthalf + case 6: self = .linkProvideSecondhalf + default: self = .UNRECOGNIZED(rawValue) + } + } + + public var rawValue: Int { + switch self { + case .canonAnnounce: return 0 + case .chainQuery: return 1 + case .linkRequest: return 3 + case .linkProvide: return 4 + case .linkProvideFirsthalf: return 5 + case .linkProvideSecondhalf: return 6 + case .UNRECOGNIZED(let i): return i + } + } + + // The compiler won't synthesize support with the UNRECOGNIZED case. + public static let allCases: [StoreForwardPlusPlus.SFPP_message_type] = [ + .canonAnnounce, + .chainQuery, + .linkRequest, + .linkProvide, + .linkProvideFirsthalf, + .linkProvideSecondhalf, + ] + + } + + public init() {} +} + /// /// Waypoint message, used to share arbitrary locations across the mesh -public struct Waypoint { +public struct Waypoint: 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. @@ -2178,7 +2275,7 @@ public struct Waypoint { /// /// This message will be proxied over the PhoneAPI for the client to deliver to the MQTT server -public struct MqttClientProxyMessage { +public struct MqttClientProxyMessage: 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. @@ -2219,7 +2316,7 @@ public struct MqttClientProxyMessage { /// /// The actual service envelope payload or text for mqtt pub / sub - public enum OneOf_PayloadVariant: Equatable { + public enum OneOf_PayloadVariant: Equatable, Sendable { /// /// Bytes case data(Data) @@ -2227,24 +2324,6 @@ public struct MqttClientProxyMessage { /// Text case text(String) - #if !swift(>=4.1) - public static func ==(lhs: MqttClientProxyMessage.OneOf_PayloadVariant, rhs: MqttClientProxyMessage.OneOf_PayloadVariant) -> Bool { - // 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 (lhs, rhs) { - case (.data, .data): return { - guard case .data(let l) = lhs, case .data(let r) = rhs else { preconditionFailure() } - return l == r - }() - case (.text, .text): return { - guard case .text(let l) = lhs, case .text(let r) = rhs else { preconditionFailure() } - return l == r - }() - default: return false - } - } - #endif } public init() {} @@ -2254,7 +2333,7 @@ public struct MqttClientProxyMessage { /// A packet envelope sent/received over the mesh /// only payload_variant is sent in the payload portion of the LORA packet. /// The other fields are either not sent at all, or sent in the special 16 byte LORA header. -public struct MeshPacket { +public struct MeshPacket: @unchecked 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. @@ -2270,6 +2349,10 @@ public struct MeshPacket { /// /// The (immediate) destination for this packet + /// If the value is 4,294,967,295 (maximum value of an unsigned 32bit integer), this indicates that the packet was + /// not destined for a specific node, but for a channel as indicated by the value of `channel` below. + /// If the value is another, this indicates that the packet was destined for a specific + /// node (i.e. a kind of "Direct Message" to this node) and not broadcast on a channel. public var to: UInt32 { get {return _storage._to} set {_uniqueStorage()._to = newValue} @@ -2388,6 +2471,8 @@ public struct MeshPacket { /// /// Describe if this message is delayed + /// + /// NOTE: This field was marked as deprecated in the .proto file. public var delayed: MeshPacket.Delayed { get {return _storage._delayed} set {_uniqueStorage()._delayed = newValue} @@ -2456,7 +2541,7 @@ public struct MeshPacket { public var unknownFields = SwiftProtobuf.UnknownStorage() - public enum OneOf_PayloadVariant: Equatable { + public enum OneOf_PayloadVariant: Equatable, Sendable { /// /// TODO: REPLACE case decoded(DataMessage) @@ -2464,24 +2549,6 @@ public struct MeshPacket { /// TODO: REPLACE case encrypted(Data) - #if !swift(>=4.1) - public static func ==(lhs: MeshPacket.OneOf_PayloadVariant, rhs: MeshPacket.OneOf_PayloadVariant) -> Bool { - // 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 (lhs, rhs) { - case (.decoded, .decoded): return { - guard case .decoded(let l) = lhs, case .decoded(let r) = rhs else { preconditionFailure() } - return l == r - }() - case (.encrypted, .encrypted): return { - guard case .encrypted(let l) = lhs, case .encrypted(let r) = rhs else { preconditionFailure() } - return l == r - }() - default: return false - } - } - #endif } /// @@ -2503,7 +2570,7 @@ public struct MeshPacket { /// So I bit the bullet and implemented a new (internal - not sent over the air) /// field in MeshPacket called 'priority'. /// And the transmission queue in the router object is now a priority queue. - public enum Priority: SwiftProtobuf.Enum { + public enum Priority: SwiftProtobuf.Enum, Swift.CaseIterable { public typealias RawValue = Int /// @@ -2587,11 +2654,25 @@ public struct MeshPacket { } } + // The compiler won't synthesize support with the UNRECOGNIZED case. + public static let allCases: [MeshPacket.Priority] = [ + .unset, + .min, + .background, + .default, + .reliable, + .response, + .high, + .alert, + .ack, + .max, + ] + } /// /// Identify if this is a delayed packet - public enum Delayed: SwiftProtobuf.Enum { + public enum Delayed: SwiftProtobuf.Enum, Swift.CaseIterable { public typealias RawValue = Int /// @@ -2629,11 +2710,18 @@ public struct MeshPacket { } } + // The compiler won't synthesize support with the UNRECOGNIZED case. + public static let allCases: [MeshPacket.Delayed] = [ + .noDelay, + .broadcast, + .direct, + ] + } /// /// Enum to identify which transport mechanism this packet arrived over - public enum TransportMechanism: SwiftProtobuf.Enum { + public enum TransportMechanism: SwiftProtobuf.Enum, Swift.CaseIterable { public typealias RawValue = Int /// @@ -2701,6 +2789,18 @@ public struct MeshPacket { } } + // The compiler won't synthesize support with the UNRECOGNIZED case. + public static let allCases: [MeshPacket.TransportMechanism] = [ + .transportInternal, + .transportLora, + .transportLoraAlt1, + .transportLoraAlt2, + .transportLoraAlt3, + .transportMqtt, + .transportMulticastUdp, + .transportApi, + ] + } public init() {} @@ -2708,49 +2808,6 @@ public struct MeshPacket { fileprivate var _storage = _StorageClass.defaultInstance } -#if swift(>=4.2) - -extension MeshPacket.Priority: CaseIterable { - // The compiler won't synthesize support with the UNRECOGNIZED case. - public static let allCases: [MeshPacket.Priority] = [ - .unset, - .min, - .background, - .default, - .reliable, - .response, - .high, - .alert, - .ack, - .max, - ] -} - -extension MeshPacket.Delayed: CaseIterable { - // The compiler won't synthesize support with the UNRECOGNIZED case. - public static let allCases: [MeshPacket.Delayed] = [ - .noDelay, - .broadcast, - .direct, - ] -} - -extension MeshPacket.TransportMechanism: CaseIterable { - // The compiler won't synthesize support with the UNRECOGNIZED case. - public static let allCases: [MeshPacket.TransportMechanism] = [ - .transportInternal, - .transportLora, - .transportLoraAlt1, - .transportLoraAlt2, - .transportLoraAlt3, - .transportMqtt, - .transportMulticastUdp, - .transportApi, - ] -} - -#endif // swift(>=4.2) - /// /// The bluetooth to device link: /// Old BTLE protocol docs from TODO, merge in above and make real docs... @@ -2768,7 +2825,7 @@ extension MeshPacket.TransportMechanism: CaseIterable { /// level etc) SET_CONFIG (switches device to a new set of radio params and /// preshared key, drops all existing nodes, force our node to rejoin this new group) /// Full information about a node on the mesh -public struct NodeInfo { +public struct NodeInfo: @unchecked 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. @@ -2890,7 +2947,7 @@ public struct NodeInfo { /// Unique local debugging info for this node /// Note: we don't include position or the user info, because that will come in the /// Sent to the phone in response to WantNodes. -public struct MyNodeInfo { +public struct MyNodeInfo: 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. @@ -2938,7 +2995,7 @@ public struct MyNodeInfo { /// on the message it is assumed to be a continuation of the previously sent message. /// This allows the device code to use fixed maxlen 64 byte strings for messages, /// and then extend as needed by emitting multiple records. -public struct LogRecord { +public struct LogRecord: 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. @@ -2963,7 +3020,7 @@ public struct LogRecord { /// /// Log levels, chosen to match python logging conventions. - public enum Level: SwiftProtobuf.Enum { + public enum Level: SwiftProtobuf.Enum, Swift.CaseIterable { public typealias RawValue = Int /// @@ -3025,29 +3082,23 @@ public struct LogRecord { } } + // The compiler won't synthesize support with the UNRECOGNIZED case. + public static let allCases: [LogRecord.Level] = [ + .unset, + .critical, + .error, + .warning, + .info, + .debug, + .trace, + ] + } public init() {} } -#if swift(>=4.2) - -extension LogRecord.Level: CaseIterable { - // The compiler won't synthesize support with the UNRECOGNIZED case. - public static let allCases: [LogRecord.Level] = [ - .unset, - .critical, - .error, - .warning, - .info, - .debug, - .trace, - ] -} - -#endif // swift(>=4.2) - -public struct QueueStatus { +public struct QueueStatus: 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. @@ -3074,7 +3125,7 @@ public struct QueueStatus { /// It will support READ and NOTIFY. When a new packet arrives the device will BLE notify? /// It will sit in that descriptor until consumed by the phone, /// at which point the next item in the FIFO will be populated. -public struct FromRadio { +public struct FromRadio: 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. @@ -3260,7 +3311,7 @@ public struct FromRadio { /// /// Log levels, chosen to match python logging conventions. - public enum OneOf_PayloadVariant: Equatable { + public enum OneOf_PayloadVariant: Equatable, Sendable { /// /// Log levels, chosen to match python logging conventions. case packet(MeshPacket) @@ -3318,80 +3369,6 @@ public struct FromRadio { /// Persistent data for device-ui case deviceuiConfig(DeviceUIConfig) - #if !swift(>=4.1) - public static func ==(lhs: FromRadio.OneOf_PayloadVariant, rhs: FromRadio.OneOf_PayloadVariant) -> Bool { - // 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 (lhs, rhs) { - case (.packet, .packet): return { - guard case .packet(let l) = lhs, case .packet(let r) = rhs else { preconditionFailure() } - return l == r - }() - case (.myInfo, .myInfo): return { - guard case .myInfo(let l) = lhs, case .myInfo(let r) = rhs else { preconditionFailure() } - return l == r - }() - case (.nodeInfo, .nodeInfo): return { - guard case .nodeInfo(let l) = lhs, case .nodeInfo(let r) = rhs else { preconditionFailure() } - return l == r - }() - case (.config, .config): return { - guard case .config(let l) = lhs, case .config(let r) = rhs else { preconditionFailure() } - return l == r - }() - case (.logRecord, .logRecord): return { - guard case .logRecord(let l) = lhs, case .logRecord(let r) = rhs else { preconditionFailure() } - return l == r - }() - case (.configCompleteID, .configCompleteID): return { - guard case .configCompleteID(let l) = lhs, case .configCompleteID(let r) = rhs else { preconditionFailure() } - return l == r - }() - case (.rebooted, .rebooted): return { - guard case .rebooted(let l) = lhs, case .rebooted(let r) = rhs else { preconditionFailure() } - return l == r - }() - case (.moduleConfig, .moduleConfig): return { - guard case .moduleConfig(let l) = lhs, case .moduleConfig(let r) = rhs else { preconditionFailure() } - return l == r - }() - case (.channel, .channel): return { - guard case .channel(let l) = lhs, case .channel(let r) = rhs else { preconditionFailure() } - return l == r - }() - case (.queueStatus, .queueStatus): return { - guard case .queueStatus(let l) = lhs, case .queueStatus(let r) = rhs else { preconditionFailure() } - return l == r - }() - case (.xmodemPacket, .xmodemPacket): return { - guard case .xmodemPacket(let l) = lhs, case .xmodemPacket(let r) = rhs else { preconditionFailure() } - return l == r - }() - case (.metadata, .metadata): return { - guard case .metadata(let l) = lhs, case .metadata(let r) = rhs else { preconditionFailure() } - return l == r - }() - case (.mqttClientProxyMessage, .mqttClientProxyMessage): return { - guard case .mqttClientProxyMessage(let l) = lhs, case .mqttClientProxyMessage(let r) = rhs else { preconditionFailure() } - return l == r - }() - case (.fileInfo, .fileInfo): return { - guard case .fileInfo(let l) = lhs, case .fileInfo(let r) = rhs else { preconditionFailure() } - return l == r - }() - case (.clientNotification, .clientNotification): return { - guard case .clientNotification(let l) = lhs, case .clientNotification(let r) = rhs else { preconditionFailure() } - return l == r - }() - case (.deviceuiConfig, .deviceuiConfig): return { - guard case .deviceuiConfig(let l) = lhs, case .deviceuiConfig(let r) = rhs else { preconditionFailure() } - return l == r - }() - default: return false - } - } - #endif } public init() {} @@ -3402,7 +3379,7 @@ public struct FromRadio { /// To be used for important messages that should to be displayed to the user /// in the form of push notifications or validation messages when saving /// invalid configuration. -public struct ClientNotification { +public struct ClientNotification: 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. @@ -3474,43 +3451,13 @@ public struct ClientNotification { public var unknownFields = SwiftProtobuf.UnknownStorage() - public enum OneOf_PayloadVariant: Equatable { + public enum OneOf_PayloadVariant: Equatable, Sendable { case keyVerificationNumberInform(KeyVerificationNumberInform) case keyVerificationNumberRequest(KeyVerificationNumberRequest) case keyVerificationFinal(KeyVerificationFinal) case duplicatedPublicKey(DuplicatedPublicKey) case lowEntropyKey(LowEntropyKey) - #if !swift(>=4.1) - public static func ==(lhs: ClientNotification.OneOf_PayloadVariant, rhs: ClientNotification.OneOf_PayloadVariant) -> Bool { - // 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 (lhs, rhs) { - case (.keyVerificationNumberInform, .keyVerificationNumberInform): return { - guard case .keyVerificationNumberInform(let l) = lhs, case .keyVerificationNumberInform(let r) = rhs else { preconditionFailure() } - return l == r - }() - case (.keyVerificationNumberRequest, .keyVerificationNumberRequest): return { - guard case .keyVerificationNumberRequest(let l) = lhs, case .keyVerificationNumberRequest(let r) = rhs else { preconditionFailure() } - return l == r - }() - case (.keyVerificationFinal, .keyVerificationFinal): return { - guard case .keyVerificationFinal(let l) = lhs, case .keyVerificationFinal(let r) = rhs else { preconditionFailure() } - return l == r - }() - case (.duplicatedPublicKey, .duplicatedPublicKey): return { - guard case .duplicatedPublicKey(let l) = lhs, case .duplicatedPublicKey(let r) = rhs else { preconditionFailure() } - return l == r - }() - case (.lowEntropyKey, .lowEntropyKey): return { - guard case .lowEntropyKey(let l) = lhs, case .lowEntropyKey(let r) = rhs else { preconditionFailure() } - return l == r - }() - default: return false - } - } - #endif } public init() {} @@ -3518,7 +3465,7 @@ public struct ClientNotification { fileprivate var _replyID: UInt32? = nil } -public struct KeyVerificationNumberInform { +public struct KeyVerificationNumberInform: 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. @@ -3534,7 +3481,7 @@ public struct KeyVerificationNumberInform { public init() {} } -public struct KeyVerificationNumberRequest { +public struct KeyVerificationNumberRequest: 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. @@ -3548,7 +3495,7 @@ public struct KeyVerificationNumberRequest { public init() {} } -public struct KeyVerificationFinal { +public struct KeyVerificationFinal: 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. @@ -3566,7 +3513,7 @@ public struct KeyVerificationFinal { public init() {} } -public struct DuplicatedPublicKey { +public struct DuplicatedPublicKey: 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. @@ -3576,7 +3523,7 @@ public struct DuplicatedPublicKey { public init() {} } -public struct LowEntropyKey { +public struct LowEntropyKey: 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. @@ -3588,7 +3535,7 @@ public struct LowEntropyKey { /// /// Individual File info for the device -public struct FileInfo { +public struct FileInfo: 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. @@ -3609,7 +3556,7 @@ public struct FileInfo { /// /// Packets/commands to the radio will be written (reliably) to the toRadio characteristic. /// Once the write completes the phone can assume it is handled. -public struct ToRadio { +public struct ToRadio: 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. @@ -3689,7 +3636,7 @@ public struct ToRadio { /// /// Log levels, chosen to match python logging conventions. - public enum OneOf_PayloadVariant: Equatable { + public enum OneOf_PayloadVariant: Equatable, Sendable { /// /// Send this packet on the mesh case packet(MeshPacket) @@ -3716,40 +3663,6 @@ public struct ToRadio { /// Heartbeat message (used to keep the device connection awake on serial) case heartbeat(Heartbeat) - #if !swift(>=4.1) - public static func ==(lhs: ToRadio.OneOf_PayloadVariant, rhs: ToRadio.OneOf_PayloadVariant) -> Bool { - // 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 (lhs, rhs) { - case (.packet, .packet): return { - guard case .packet(let l) = lhs, case .packet(let r) = rhs else { preconditionFailure() } - return l == r - }() - case (.wantConfigID, .wantConfigID): return { - guard case .wantConfigID(let l) = lhs, case .wantConfigID(let r) = rhs else { preconditionFailure() } - return l == r - }() - case (.disconnect, .disconnect): return { - guard case .disconnect(let l) = lhs, case .disconnect(let r) = rhs else { preconditionFailure() } - return l == r - }() - case (.xmodemPacket, .xmodemPacket): return { - guard case .xmodemPacket(let l) = lhs, case .xmodemPacket(let r) = rhs else { preconditionFailure() } - return l == r - }() - case (.mqttClientProxyMessage, .mqttClientProxyMessage): return { - guard case .mqttClientProxyMessage(let l) = lhs, case .mqttClientProxyMessage(let r) = rhs else { preconditionFailure() } - return l == r - }() - case (.heartbeat, .heartbeat): return { - guard case .heartbeat(let l) = lhs, case .heartbeat(let r) = rhs else { preconditionFailure() } - return l == r - }() - default: return false - } - } - #endif } public init() {} @@ -3757,7 +3670,7 @@ public struct ToRadio { /// /// Compressed message payload -public struct Compressed { +public struct Compressed: 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. @@ -3777,7 +3690,7 @@ public struct Compressed { /// /// Full info on edges for a single node -public struct NeighborInfo { +public struct NeighborInfo: 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. @@ -3805,7 +3718,7 @@ public struct NeighborInfo { /// /// A single edge in the mesh -public struct Neighbor { +public struct Neighbor: 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. @@ -3835,7 +3748,7 @@ public struct Neighbor { /// /// Device metadata response -public struct DeviceMetadata { +public struct DeviceMetadata: 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. @@ -3897,7 +3810,7 @@ public struct DeviceMetadata { /// /// A heartbeat message is sent to the node from the client to keep the connection alive. /// This is currently only needed to keep serial connections alive, but can be used by any PhoneAPI. -public struct Heartbeat { +public struct Heartbeat: 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. @@ -3913,7 +3826,7 @@ public struct Heartbeat { /// /// RemoteHardwarePins associated with a node -public struct NodeRemoteHardwarePin { +public struct NodeRemoteHardwarePin: 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. @@ -3940,7 +3853,7 @@ public struct NodeRemoteHardwarePin { fileprivate var _pin: RemoteHardwarePin? = nil } -public struct ChunkedPayload { +public struct ChunkedPayload: 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. @@ -3968,7 +3881,7 @@ public struct ChunkedPayload { /// /// Wrapper message for broken repeated oneof support -public struct resend_chunks { +public struct resend_chunks: 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. @@ -3982,7 +3895,7 @@ public struct resend_chunks { /// /// Responses to a ChunkedPayload request -public struct ChunkedPayloadResponse { +public struct ChunkedPayloadResponse: 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. @@ -4025,7 +3938,7 @@ public struct ChunkedPayloadResponse { public var unknownFields = SwiftProtobuf.UnknownStorage() - public enum OneOf_PayloadVariant: Equatable { + public enum OneOf_PayloadVariant: Equatable, Sendable { /// /// Request to transfer chunked payload case requestTransfer(Bool) @@ -4036,297 +3949,38 @@ public struct ChunkedPayloadResponse { /// Request missing indexes in the chunked payload case resendChunks(resend_chunks) - #if !swift(>=4.1) - public static func ==(lhs: ChunkedPayloadResponse.OneOf_PayloadVariant, rhs: ChunkedPayloadResponse.OneOf_PayloadVariant) -> Bool { - // 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 (lhs, rhs) { - case (.requestTransfer, .requestTransfer): return { - guard case .requestTransfer(let l) = lhs, case .requestTransfer(let r) = rhs else { preconditionFailure() } - return l == r - }() - case (.acceptTransfer, .acceptTransfer): return { - guard case .acceptTransfer(let l) = lhs, case .acceptTransfer(let r) = rhs else { preconditionFailure() } - return l == r - }() - case (.resendChunks, .resendChunks): return { - guard case .resendChunks(let l) = lhs, case .resendChunks(let r) = rhs else { preconditionFailure() } - return l == r - }() - default: return false - } - } - #endif } public init() {} } -#if swift(>=5.5) && canImport(_Concurrency) -extension HardwareModel: @unchecked Sendable {} -extension Constants: @unchecked Sendable {} -extension CriticalErrorCode: @unchecked Sendable {} -extension FirmwareEdition: @unchecked Sendable {} -extension ExcludedModules: @unchecked Sendable {} -extension Position: @unchecked Sendable {} -extension Position.LocSource: @unchecked Sendable {} -extension Position.AltSource: @unchecked Sendable {} -extension User: @unchecked Sendable {} -extension RouteDiscovery: @unchecked Sendable {} -extension Routing: @unchecked Sendable {} -extension Routing.OneOf_Variant: @unchecked Sendable {} -extension Routing.Error: @unchecked Sendable {} -extension DataMessage: @unchecked Sendable {} -extension KeyVerification: @unchecked Sendable {} -extension Waypoint: @unchecked Sendable {} -extension MqttClientProxyMessage: @unchecked Sendable {} -extension MqttClientProxyMessage.OneOf_PayloadVariant: @unchecked Sendable {} -extension MeshPacket: @unchecked Sendable {} -extension MeshPacket.OneOf_PayloadVariant: @unchecked Sendable {} -extension MeshPacket.Priority: @unchecked Sendable {} -extension MeshPacket.Delayed: @unchecked Sendable {} -extension MeshPacket.TransportMechanism: @unchecked Sendable {} -extension NodeInfo: @unchecked Sendable {} -extension MyNodeInfo: @unchecked Sendable {} -extension LogRecord: @unchecked Sendable {} -extension LogRecord.Level: @unchecked Sendable {} -extension QueueStatus: @unchecked Sendable {} -extension FromRadio: @unchecked Sendable {} -extension FromRadio.OneOf_PayloadVariant: @unchecked Sendable {} -extension ClientNotification: @unchecked Sendable {} -extension ClientNotification.OneOf_PayloadVariant: @unchecked Sendable {} -extension KeyVerificationNumberInform: @unchecked Sendable {} -extension KeyVerificationNumberRequest: @unchecked Sendable {} -extension KeyVerificationFinal: @unchecked Sendable {} -extension DuplicatedPublicKey: @unchecked Sendable {} -extension LowEntropyKey: @unchecked Sendable {} -extension FileInfo: @unchecked Sendable {} -extension ToRadio: @unchecked Sendable {} -extension ToRadio.OneOf_PayloadVariant: @unchecked Sendable {} -extension Compressed: @unchecked Sendable {} -extension NeighborInfo: @unchecked Sendable {} -extension Neighbor: @unchecked Sendable {} -extension DeviceMetadata: @unchecked Sendable {} -extension Heartbeat: @unchecked Sendable {} -extension NodeRemoteHardwarePin: @unchecked Sendable {} -extension ChunkedPayload: @unchecked Sendable {} -extension resend_chunks: @unchecked Sendable {} -extension ChunkedPayloadResponse: @unchecked Sendable {} -extension ChunkedPayloadResponse.OneOf_PayloadVariant: @unchecked Sendable {} -#endif // swift(>=5.5) && canImport(_Concurrency) - // MARK: - Code below here is support for the SwiftProtobuf runtime. fileprivate let _protobuf_package = "meshtastic" extension HardwareModel: SwiftProtobuf._ProtoNameProviding { - public static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ - 0: .same(proto: "UNSET"), - 1: .same(proto: "TLORA_V2"), - 2: .same(proto: "TLORA_V1"), - 3: .same(proto: "TLORA_V2_1_1P6"), - 4: .same(proto: "TBEAM"), - 5: .same(proto: "HELTEC_V2_0"), - 6: .same(proto: "TBEAM_V0P7"), - 7: .same(proto: "T_ECHO"), - 8: .same(proto: "TLORA_V1_1P3"), - 9: .same(proto: "RAK4631"), - 10: .same(proto: "HELTEC_V2_1"), - 11: .same(proto: "HELTEC_V1"), - 12: .same(proto: "LILYGO_TBEAM_S3_CORE"), - 13: .same(proto: "RAK11200"), - 14: .same(proto: "NANO_G1"), - 15: .same(proto: "TLORA_V2_1_1P8"), - 16: .same(proto: "TLORA_T3_S3"), - 17: .same(proto: "NANO_G1_EXPLORER"), - 18: .same(proto: "NANO_G2_ULTRA"), - 19: .same(proto: "LORA_TYPE"), - 20: .same(proto: "WIPHONE"), - 21: .same(proto: "WIO_WM1110"), - 22: .same(proto: "RAK2560"), - 23: .same(proto: "HELTEC_HRU_3601"), - 24: .same(proto: "HELTEC_WIRELESS_BRIDGE"), - 25: .same(proto: "STATION_G1"), - 26: .same(proto: "RAK11310"), - 27: .same(proto: "SENSELORA_RP2040"), - 28: .same(proto: "SENSELORA_S3"), - 29: .same(proto: "CANARYONE"), - 30: .same(proto: "RP2040_LORA"), - 31: .same(proto: "STATION_G2"), - 32: .same(proto: "LORA_RELAY_V1"), - 33: .same(proto: "NRF52840DK"), - 34: .same(proto: "PPR"), - 35: .same(proto: "GENIEBLOCKS"), - 36: .same(proto: "NRF52_UNKNOWN"), - 37: .same(proto: "PORTDUINO"), - 38: .same(proto: "ANDROID_SIM"), - 39: .same(proto: "DIY_V1"), - 40: .same(proto: "NRF52840_PCA10059"), - 41: .same(proto: "DR_DEV"), - 42: .same(proto: "M5STACK"), - 43: .same(proto: "HELTEC_V3"), - 44: .same(proto: "HELTEC_WSL_V3"), - 45: .same(proto: "BETAFPV_2400_TX"), - 46: .same(proto: "BETAFPV_900_NANO_TX"), - 47: .same(proto: "RPI_PICO"), - 48: .same(proto: "HELTEC_WIRELESS_TRACKER"), - 49: .same(proto: "HELTEC_WIRELESS_PAPER"), - 50: .same(proto: "T_DECK"), - 51: .same(proto: "T_WATCH_S3"), - 52: .same(proto: "PICOMPUTER_S3"), - 53: .same(proto: "HELTEC_HT62"), - 54: .same(proto: "EBYTE_ESP32_S3"), - 55: .same(proto: "ESP32_S3_PICO"), - 56: .same(proto: "CHATTER_2"), - 57: .same(proto: "HELTEC_WIRELESS_PAPER_V1_0"), - 58: .same(proto: "HELTEC_WIRELESS_TRACKER_V1_0"), - 59: .same(proto: "UNPHONE"), - 60: .same(proto: "TD_LORAC"), - 61: .same(proto: "CDEBYTE_EORA_S3"), - 62: .same(proto: "TWC_MESH_V4"), - 63: .same(proto: "NRF52_PROMICRO_DIY"), - 64: .same(proto: "RADIOMASTER_900_BANDIT_NANO"), - 65: .same(proto: "HELTEC_CAPSULE_SENSOR_V3"), - 66: .same(proto: "HELTEC_VISION_MASTER_T190"), - 67: .same(proto: "HELTEC_VISION_MASTER_E213"), - 68: .same(proto: "HELTEC_VISION_MASTER_E290"), - 69: .same(proto: "HELTEC_MESH_NODE_T114"), - 70: .same(proto: "SENSECAP_INDICATOR"), - 71: .same(proto: "TRACKER_T1000_E"), - 72: .same(proto: "RAK3172"), - 73: .same(proto: "WIO_E5"), - 74: .same(proto: "RADIOMASTER_900_BANDIT"), - 75: .same(proto: "ME25LS01_4Y10TD"), - 76: .same(proto: "RP2040_FEATHER_RFM95"), - 77: .same(proto: "M5STACK_COREBASIC"), - 78: .same(proto: "M5STACK_CORE2"), - 79: .same(proto: "RPI_PICO2"), - 80: .same(proto: "M5STACK_CORES3"), - 81: .same(proto: "SEEED_XIAO_S3"), - 82: .same(proto: "MS24SF1"), - 83: .same(proto: "TLORA_C6"), - 84: .same(proto: "WISMESH_TAP"), - 85: .same(proto: "ROUTASTIC"), - 86: .same(proto: "MESH_TAB"), - 87: .same(proto: "MESHLINK"), - 88: .same(proto: "XIAO_NRF52_KIT"), - 89: .same(proto: "THINKNODE_M1"), - 90: .same(proto: "THINKNODE_M2"), - 91: .same(proto: "T_ETH_ELITE"), - 92: .same(proto: "HELTEC_SENSOR_HUB"), - 93: .same(proto: "RESERVED_FRIED_CHICKEN"), - 94: .same(proto: "HELTEC_MESH_POCKET"), - 95: .same(proto: "SEEED_SOLAR_NODE"), - 96: .same(proto: "NOMADSTAR_METEOR_PRO"), - 97: .same(proto: "CROWPANEL"), - 98: .same(proto: "LINK_32"), - 99: .same(proto: "SEEED_WIO_TRACKER_L1"), - 100: .same(proto: "SEEED_WIO_TRACKER_L1_EINK"), - 101: .same(proto: "MUZI_R1_NEO"), - 102: .same(proto: "T_DECK_PRO"), - 103: .same(proto: "T_LORA_PAGER"), - 104: .same(proto: "M5STACK_RESERVED"), - 105: .same(proto: "WISMESH_TAG"), - 106: .same(proto: "RAK3312"), - 107: .same(proto: "THINKNODE_M5"), - 108: .same(proto: "HELTEC_MESH_SOLAR"), - 109: .same(proto: "T_ECHO_LITE"), - 110: .same(proto: "HELTEC_V4"), - 111: .same(proto: "M5STACK_C6L"), - 112: .same(proto: "M5STACK_CARDPUTER_ADV"), - 113: .same(proto: "HELTEC_WIRELESS_TRACKER_V2"), - 114: .same(proto: "T_WATCH_ULTRA"), - 255: .same(proto: "PRIVATE_HW"), - ] + 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 { - public static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ - 0: .same(proto: "ZERO"), - 233: .same(proto: "DATA_PAYLOAD_LEN"), - ] + public static let _protobuf_nameMap = SwiftProtobuf._NameMap(bytecode: "\0\u{2}\0ZERO\0\u{2}i\u{3}DATA_PAYLOAD_LEN\0") } extension CriticalErrorCode: SwiftProtobuf._ProtoNameProviding { - public static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ - 0: .same(proto: "NONE"), - 1: .same(proto: "TX_WATCHDOG"), - 2: .same(proto: "SLEEP_ENTER_WAIT"), - 3: .same(proto: "NO_RADIO"), - 4: .same(proto: "UNSPECIFIED"), - 5: .same(proto: "UBLOX_UNIT_FAILED"), - 6: .same(proto: "NO_AXP192"), - 7: .same(proto: "INVALID_RADIO_SETTING"), - 8: .same(proto: "TRANSMIT_FAILED"), - 9: .same(proto: "BROWNOUT"), - 10: .same(proto: "SX1262_FAILURE"), - 11: .same(proto: "RADIO_SPI_BUG"), - 12: .same(proto: "FLASH_CORRUPTION_RECOVERABLE"), - 13: .same(proto: "FLASH_CORRUPTION_UNRECOVERABLE"), - ] + public static let _protobuf_nameMap = SwiftProtobuf._NameMap(bytecode: "\0\u{2}\0NONE\0\u{1}TX_WATCHDOG\0\u{1}SLEEP_ENTER_WAIT\0\u{1}NO_RADIO\0\u{1}UNSPECIFIED\0\u{1}UBLOX_UNIT_FAILED\0\u{1}NO_AXP192\0\u{1}INVALID_RADIO_SETTING\0\u{1}TRANSMIT_FAILED\0\u{1}BROWNOUT\0\u{1}SX1262_FAILURE\0\u{1}RADIO_SPI_BUG\0\u{1}FLASH_CORRUPTION_RECOVERABLE\0\u{1}FLASH_CORRUPTION_UNRECOVERABLE\0") } extension FirmwareEdition: SwiftProtobuf._ProtoNameProviding { - public static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ - 0: .same(proto: "VANILLA"), - 1: .same(proto: "SMART_CITIZEN"), - 16: .same(proto: "OPEN_SAUCE"), - 17: .same(proto: "DEFCON"), - 18: .same(proto: "BURNING_MAN"), - 19: .same(proto: "HAMVENTION"), - 127: .same(proto: "DIY_EDITION"), - ] + public static let _protobuf_nameMap = SwiftProtobuf._NameMap(bytecode: "\0\u{2}\0VANILLA\0\u{1}SMART_CITIZEN\0\u{2}\u{f}OPEN_SAUCE\0\u{1}DEFCON\0\u{1}BURNING_MAN\0\u{1}HAMVENTION\0\u{2}l\u{1}DIY_EDITION\0") } extension ExcludedModules: SwiftProtobuf._ProtoNameProviding { - public static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ - 0: .same(proto: "EXCLUDED_NONE"), - 1: .same(proto: "MQTT_CONFIG"), - 2: .same(proto: "SERIAL_CONFIG"), - 4: .same(proto: "EXTNOTIF_CONFIG"), - 8: .same(proto: "STOREFORWARD_CONFIG"), - 16: .same(proto: "RANGETEST_CONFIG"), - 32: .same(proto: "TELEMETRY_CONFIG"), - 64: .same(proto: "CANNEDMSG_CONFIG"), - 128: .same(proto: "AUDIO_CONFIG"), - 256: .same(proto: "REMOTEHARDWARE_CONFIG"), - 512: .same(proto: "NEIGHBORINFO_CONFIG"), - 1024: .same(proto: "AMBIENTLIGHTING_CONFIG"), - 2048: .same(proto: "DETECTIONSENSOR_CONFIG"), - 4096: .same(proto: "PAXCOUNTER_CONFIG"), - 8192: .same(proto: "BLUETOOTH_CONFIG"), - 16384: .same(proto: "NETWORK_CONFIG"), - ] + public static let _protobuf_nameMap = SwiftProtobuf._NameMap(bytecode: "\0\u{2}\0EXCLUDED_NONE\0\u{1}MQTT_CONFIG\0\u{1}SERIAL_CONFIG\0\u{2}\u{2}EXTNOTIF_CONFIG\0\u{2}\u{4}STOREFORWARD_CONFIG\0\u{2}\u{8}RANGETEST_CONFIG\0\u{2}\u{10}TELEMETRY_CONFIG\0\u{2} CANNEDMSG_CONFIG\0\u{2}@\u{1}AUDIO_CONFIG\0\u{2}@\u{2}REMOTEHARDWARE_CONFIG\0\u{2}@\u{4}NEIGHBORINFO_CONFIG\0\u{2}@\u{8}AMBIENTLIGHTING_CONFIG\0\u{2}@\u{10}DETECTIONSENSOR_CONFIG\0\u{2}@ PAXCOUNTER_CONFIG\0\u{2}@@\u{1}BLUETOOTH_CONFIG\0\u{2}@@\u{2}NETWORK_CONFIG\0") } extension Position: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { public static let protoMessageName: String = _protobuf_package + ".Position" - public static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ - 1: .standard(proto: "latitude_i"), - 2: .standard(proto: "longitude_i"), - 3: .same(proto: "altitude"), - 4: .same(proto: "time"), - 5: .standard(proto: "location_source"), - 6: .standard(proto: "altitude_source"), - 7: .same(proto: "timestamp"), - 8: .standard(proto: "timestamp_millis_adjust"), - 9: .standard(proto: "altitude_hae"), - 10: .standard(proto: "altitude_geoidal_separation"), - 11: .same(proto: "PDOP"), - 12: .same(proto: "HDOP"), - 13: .same(proto: "VDOP"), - 14: .standard(proto: "gps_accuracy"), - 15: .standard(proto: "ground_speed"), - 16: .standard(proto: "ground_track"), - 17: .standard(proto: "fix_quality"), - 18: .standard(proto: "fix_type"), - 19: .standard(proto: "sats_in_view"), - 20: .standard(proto: "sensor_id"), - 21: .standard(proto: "next_update"), - 22: .standard(proto: "seq_number"), - 23: .standard(proto: "precision_bits"), - ] + public static let _protobuf_nameMap = SwiftProtobuf._NameMap(bytecode: "\0\u{3}latitude_i\0\u{3}longitude_i\0\u{1}altitude\0\u{1}time\0\u{3}location_source\0\u{3}altitude_source\0\u{1}timestamp\0\u{3}timestamp_millis_adjust\0\u{3}altitude_hae\0\u{3}altitude_geoidal_separation\0\u{1}PDOP\0\u{1}HDOP\0\u{1}VDOP\0\u{3}gps_accuracy\0\u{3}ground_speed\0\u{3}ground_track\0\u{3}fix_quality\0\u{3}fix_type\0\u{3}sats_in_view\0\u{3}sensor_id\0\u{3}next_update\0\u{3}seq_number\0\u{3}precision_bits\0") fileprivate class _StorageClass { var _latitudeI: Int32? = nil @@ -4353,15 +4007,11 @@ extension Position: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationB var _seqNumber: UInt32 = 0 var _precisionBits: UInt32 = 0 - #if swift(>=5.10) // 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. // This will force a copy to be made of this reference when the first mutation occurs; // hence, it is safe to mark this as `nonisolated(unsafe)`. static nonisolated(unsafe) let defaultInstance = _StorageClass() - #else - static let defaultInstance = _StorageClass() - #endif private init() {} @@ -4553,37 +4203,16 @@ extension Position: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationB } extension Position.LocSource: SwiftProtobuf._ProtoNameProviding { - public static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ - 0: .same(proto: "LOC_UNSET"), - 1: .same(proto: "LOC_MANUAL"), - 2: .same(proto: "LOC_INTERNAL"), - 3: .same(proto: "LOC_EXTERNAL"), - ] + public static let _protobuf_nameMap = SwiftProtobuf._NameMap(bytecode: "\0\u{2}\0LOC_UNSET\0\u{1}LOC_MANUAL\0\u{1}LOC_INTERNAL\0\u{1}LOC_EXTERNAL\0") } extension Position.AltSource: SwiftProtobuf._ProtoNameProviding { - public static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ - 0: .same(proto: "ALT_UNSET"), - 1: .same(proto: "ALT_MANUAL"), - 2: .same(proto: "ALT_INTERNAL"), - 3: .same(proto: "ALT_EXTERNAL"), - 4: .same(proto: "ALT_BAROMETRIC"), - ] + public static let _protobuf_nameMap = SwiftProtobuf._NameMap(bytecode: "\0\u{2}\0ALT_UNSET\0\u{1}ALT_MANUAL\0\u{1}ALT_INTERNAL\0\u{1}ALT_EXTERNAL\0\u{1}ALT_BAROMETRIC\0") } extension User: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { public static let protoMessageName: String = _protobuf_package + ".User" - public static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ - 1: .same(proto: "id"), - 2: .standard(proto: "long_name"), - 3: .standard(proto: "short_name"), - 4: .same(proto: "macaddr"), - 5: .standard(proto: "hw_model"), - 6: .standard(proto: "is_licensed"), - 7: .same(proto: "role"), - 8: .standard(proto: "public_key"), - 9: .standard(proto: "is_unmessagable"), - ] + public static let _protobuf_nameMap = SwiftProtobuf._NameMap(bytecode: "\0\u{1}id\0\u{3}long_name\0\u{3}short_name\0\u{1}macaddr\0\u{3}hw_model\0\u{3}is_licensed\0\u{1}role\0\u{3}public_key\0\u{3}is_unmessagable\0") public mutating func decodeMessage(decoder: inout D) throws { while let fieldNumber = try decoder.nextFieldNumber() { @@ -4657,12 +4286,7 @@ extension User: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, extension RouteDiscovery: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { public static let protoMessageName: String = _protobuf_package + ".RouteDiscovery" - public static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ - 1: .same(proto: "route"), - 2: .standard(proto: "snr_towards"), - 3: .standard(proto: "route_back"), - 4: .standard(proto: "snr_back"), - ] + public static let _protobuf_nameMap = SwiftProtobuf._NameMap(bytecode: "\0\u{1}route\0\u{3}snr_towards\0\u{3}route_back\0\u{3}snr_back\0") public mutating func decodeMessage(decoder: inout D) throws { while let fieldNumber = try decoder.nextFieldNumber() { @@ -4707,11 +4331,7 @@ extension RouteDiscovery: SwiftProtobuf.Message, SwiftProtobuf._MessageImplement extension Routing: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { public static let protoMessageName: String = _protobuf_package + ".Routing" - public static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ - 1: .standard(proto: "route_request"), - 2: .standard(proto: "route_reply"), - 3: .standard(proto: "error_reason"), - ] + public static let _protobuf_nameMap = SwiftProtobuf._NameMap(bytecode: "\0\u{3}route_request\0\u{3}route_reply\0\u{3}error_reason\0") public mutating func decodeMessage(decoder: inout D) throws { while let fieldNumber = try decoder.nextFieldNumber() { @@ -4789,40 +4409,12 @@ extension Routing: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBa } extension Routing.Error: SwiftProtobuf._ProtoNameProviding { - public static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ - 0: .same(proto: "NONE"), - 1: .same(proto: "NO_ROUTE"), - 2: .same(proto: "GOT_NAK"), - 3: .same(proto: "TIMEOUT"), - 4: .same(proto: "NO_INTERFACE"), - 5: .same(proto: "MAX_RETRANSMIT"), - 6: .same(proto: "NO_CHANNEL"), - 7: .same(proto: "TOO_LARGE"), - 8: .same(proto: "NO_RESPONSE"), - 9: .same(proto: "DUTY_CYCLE_LIMIT"), - 32: .same(proto: "BAD_REQUEST"), - 33: .same(proto: "NOT_AUTHORIZED"), - 34: .same(proto: "PKI_FAILED"), - 35: .same(proto: "PKI_UNKNOWN_PUBKEY"), - 36: .same(proto: "ADMIN_BAD_SESSION_KEY"), - 37: .same(proto: "ADMIN_PUBLIC_KEY_UNAUTHORIZED"), - 38: .same(proto: "RATE_LIMIT_EXCEEDED"), - ] + 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 { public static let protoMessageName: String = _protobuf_package + ".Data" - public static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ - 1: .same(proto: "portnum"), - 2: .same(proto: "payload"), - 3: .standard(proto: "want_response"), - 4: .same(proto: "dest"), - 5: .same(proto: "source"), - 6: .standard(proto: "request_id"), - 7: .standard(proto: "reply_id"), - 8: .same(proto: "emoji"), - 9: .same(proto: "bitfield"), - ] + public static let _protobuf_nameMap = SwiftProtobuf._NameMap(bytecode: "\0\u{1}portnum\0\u{1}payload\0\u{3}want_response\0\u{1}dest\0\u{1}source\0\u{3}request_id\0\u{3}reply_id\0\u{1}emoji\0\u{1}bitfield\0") public mutating func decodeMessage(decoder: inout D) throws { while let fieldNumber = try decoder.nextFieldNumber() { @@ -4896,11 +4488,7 @@ extension DataMessage: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementati extension KeyVerification: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { public static let protoMessageName: String = _protobuf_package + ".KeyVerification" - public static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ - 1: .same(proto: "nonce"), - 2: .same(proto: "hash1"), - 3: .same(proto: "hash2"), - ] + public static let _protobuf_nameMap = SwiftProtobuf._NameMap(bytecode: "\0\u{1}nonce\0\u{1}hash1\0\u{1}hash2\0") public mutating func decodeMessage(decoder: inout D) throws { while let fieldNumber = try decoder.nextFieldNumber() { @@ -4938,18 +4526,83 @@ 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 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.sfppMessageType) }() + case 2: try { try decoder.decodeSingularBytesField(value: &self.messageHash) }() + case 3: try { try decoder.decodeSingularBytesField(value: &self.commitHash) }() + case 4: try { try decoder.decodeSingularBytesField(value: &self.rootHash) }() + case 5: try { try decoder.decodeSingularBytesField(value: &self.message) }() + case 6: try { try decoder.decodeSingularUInt32Field(value: &self.encapsulatedID) }() + 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) }() + default: break + } + } + } + + public func traverse(visitor: inout V) throws { + if self.sfppMessageType != .canonAnnounce { + try visitor.visitSingularEnumField(value: self.sfppMessageType, fieldNumber: 1) + } + if !self.messageHash.isEmpty { + try visitor.visitSingularBytesField(value: self.messageHash, fieldNumber: 2) + } + if !self.commitHash.isEmpty { + try visitor.visitSingularBytesField(value: self.commitHash, fieldNumber: 3) + } + if !self.rootHash.isEmpty { + try visitor.visitSingularBytesField(value: self.rootHash, fieldNumber: 4) + } + if !self.message.isEmpty { + try visitor.visitSingularBytesField(value: self.message, fieldNumber: 5) + } + if self.encapsulatedID != 0 { + try visitor.visitSingularUInt32Field(value: self.encapsulatedID, fieldNumber: 6) + } + if self.encapsulatedTo != 0 { + try visitor.visitSingularUInt32Field(value: self.encapsulatedTo, fieldNumber: 7) + } + if self.encapsulatedFrom != 0 { + try visitor.visitSingularUInt32Field(value: self.encapsulatedFrom, fieldNumber: 8) + } + if self.encapsulatedRxtime != 0 { + try visitor.visitSingularUInt32Field(value: self.encapsulatedRxtime, fieldNumber: 9) + } + try unknownFields.traverse(visitor: &visitor) + } + + public static func ==(lhs: StoreForwardPlusPlus, rhs: StoreForwardPlusPlus) -> Bool { + if lhs.sfppMessageType != rhs.sfppMessageType {return false} + if lhs.messageHash != rhs.messageHash {return false} + if lhs.commitHash != rhs.commitHash {return false} + if lhs.rootHash != rhs.rootHash {return false} + if lhs.message != rhs.message {return false} + if lhs.encapsulatedID != rhs.encapsulatedID {return false} + if lhs.encapsulatedTo != rhs.encapsulatedTo {return false} + if lhs.encapsulatedFrom != rhs.encapsulatedFrom {return false} + if lhs.encapsulatedRxtime != rhs.encapsulatedRxtime {return false} + if lhs.unknownFields != rhs.unknownFields {return false} + return true + } +} + +extension StoreForwardPlusPlus.SFPP_message_type: SwiftProtobuf._ProtoNameProviding { + public static let _protobuf_nameMap = SwiftProtobuf._NameMap(bytecode: "\0\u{2}\0CANON_ANNOUNCE\0\u{1}CHAIN_QUERY\0\u{2}\u{2}LINK_REQUEST\0\u{1}LINK_PROVIDE\0\u{1}LINK_PROVIDE_FIRSTHALF\0\u{1}LINK_PROVIDE_SECONDHALF\0") +} + extension Waypoint: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { public static let protoMessageName: String = _protobuf_package + ".Waypoint" - public static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ - 1: .same(proto: "id"), - 2: .standard(proto: "latitude_i"), - 3: .standard(proto: "longitude_i"), - 4: .same(proto: "expire"), - 5: .standard(proto: "locked_to"), - 6: .same(proto: "name"), - 7: .same(proto: "description"), - 8: .same(proto: "icon"), - ] + public static let _protobuf_nameMap = SwiftProtobuf._NameMap(bytecode: "\0\u{1}id\0\u{3}latitude_i\0\u{3}longitude_i\0\u{1}expire\0\u{3}locked_to\0\u{1}name\0\u{1}description\0\u{1}icon\0") public mutating func decodeMessage(decoder: inout D) throws { while let fieldNumber = try decoder.nextFieldNumber() { @@ -5018,12 +4671,7 @@ extension Waypoint: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationB extension MqttClientProxyMessage: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { public static let protoMessageName: String = _protobuf_package + ".MqttClientProxyMessage" - public static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ - 1: .same(proto: "topic"), - 2: .same(proto: "data"), - 3: .same(proto: "text"), - 4: .same(proto: "retained"), - ] + public static let _protobuf_nameMap = SwiftProtobuf._NameMap(bytecode: "\0\u{1}topic\0\u{1}data\0\u{1}text\0\u{1}retained\0") public mutating func decodeMessage(decoder: inout D) throws { while let fieldNumber = try decoder.nextFieldNumber() { @@ -5090,29 +4738,7 @@ extension MqttClientProxyMessage: SwiftProtobuf.Message, SwiftProtobuf._MessageI extension MeshPacket: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { public static let protoMessageName: String = _protobuf_package + ".MeshPacket" - public static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ - 1: .same(proto: "from"), - 2: .same(proto: "to"), - 3: .same(proto: "channel"), - 4: .same(proto: "decoded"), - 5: .same(proto: "encrypted"), - 6: .same(proto: "id"), - 7: .standard(proto: "rx_time"), - 8: .standard(proto: "rx_snr"), - 9: .standard(proto: "hop_limit"), - 10: .standard(proto: "want_ack"), - 11: .same(proto: "priority"), - 12: .standard(proto: "rx_rssi"), - 13: .same(proto: "delayed"), - 14: .standard(proto: "via_mqtt"), - 15: .standard(proto: "hop_start"), - 16: .standard(proto: "public_key"), - 17: .standard(proto: "pki_encrypted"), - 18: .standard(proto: "next_hop"), - 19: .standard(proto: "relay_node"), - 20: .standard(proto: "tx_after"), - 21: .standard(proto: "transport_mechanism"), - ] + public static let _protobuf_nameMap = SwiftProtobuf._NameMap(bytecode: "\0\u{1}from\0\u{1}to\0\u{1}channel\0\u{1}decoded\0\u{1}encrypted\0\u{1}id\0\u{3}rx_time\0\u{3}rx_snr\0\u{3}hop_limit\0\u{3}want_ack\0\u{1}priority\0\u{3}rx_rssi\0\u{1}delayed\0\u{3}via_mqtt\0\u{3}hop_start\0\u{3}public_key\0\u{3}pki_encrypted\0\u{3}next_hop\0\u{3}relay_node\0\u{3}tx_after\0\u{3}transport_mechanism\0") fileprivate class _StorageClass { var _from: UInt32 = 0 @@ -5136,15 +4762,11 @@ extension MeshPacket: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementatio var _txAfter: UInt32 = 0 var _transportMechanism: MeshPacket.TransportMechanism = .transportInternal - #if swift(>=5.10) // 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. // This will force a copy to be made of this reference when the first mutation occurs; // hence, it is safe to mark this as `nonisolated(unsafe)`. static nonisolated(unsafe) let defaultInstance = _StorageClass() - #else - static let defaultInstance = _StorageClass() - #endif private init() {} @@ -5265,7 +4887,7 @@ extension MeshPacket: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementatio if _storage._rxTime != 0 { try visitor.visitSingularFixed32Field(value: _storage._rxTime, fieldNumber: 7) } - if _storage._rxSnr != 0 { + if _storage._rxSnr.bitPattern != 0 { try visitor.visitSingularFloatField(value: _storage._rxSnr, fieldNumber: 8) } if _storage._hopLimit != 0 { @@ -5346,57 +4968,20 @@ extension MeshPacket: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementatio } extension MeshPacket.Priority: SwiftProtobuf._ProtoNameProviding { - public static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ - 0: .same(proto: "UNSET"), - 1: .same(proto: "MIN"), - 10: .same(proto: "BACKGROUND"), - 64: .same(proto: "DEFAULT"), - 70: .same(proto: "RELIABLE"), - 80: .same(proto: "RESPONSE"), - 100: .same(proto: "HIGH"), - 110: .same(proto: "ALERT"), - 120: .same(proto: "ACK"), - 127: .same(proto: "MAX"), - ] + public static let _protobuf_nameMap = SwiftProtobuf._NameMap(bytecode: "\0\u{2}\0UNSET\0\u{1}MIN\0\u{2}\u{9}BACKGROUND\0\u{2}6DEFAULT\0\u{2}\u{6}RELIABLE\0\u{2}\u{a}RESPONSE\0\u{2}\u{14}HIGH\0\u{2}\u{a}ALERT\0\u{2}\u{a}ACK\0\u{2}\u{7}MAX\0") } extension MeshPacket.Delayed: SwiftProtobuf._ProtoNameProviding { - public static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ - 0: .same(proto: "NO_DELAY"), - 1: .same(proto: "DELAYED_BROADCAST"), - 2: .same(proto: "DELAYED_DIRECT"), - ] + public static let _protobuf_nameMap = SwiftProtobuf._NameMap(bytecode: "\0\u{2}\0NO_DELAY\0\u{1}DELAYED_BROADCAST\0\u{1}DELAYED_DIRECT\0") } extension MeshPacket.TransportMechanism: SwiftProtobuf._ProtoNameProviding { - public static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ - 0: .same(proto: "TRANSPORT_INTERNAL"), - 1: .same(proto: "TRANSPORT_LORA"), - 2: .same(proto: "TRANSPORT_LORA_ALT1"), - 3: .same(proto: "TRANSPORT_LORA_ALT2"), - 4: .same(proto: "TRANSPORT_LORA_ALT3"), - 5: .same(proto: "TRANSPORT_MQTT"), - 6: .same(proto: "TRANSPORT_MULTICAST_UDP"), - 7: .same(proto: "TRANSPORT_API"), - ] + public static let _protobuf_nameMap = SwiftProtobuf._NameMap(bytecode: "\0\u{2}\0TRANSPORT_INTERNAL\0\u{1}TRANSPORT_LORA\0\u{1}TRANSPORT_LORA_ALT1\0\u{1}TRANSPORT_LORA_ALT2\0\u{1}TRANSPORT_LORA_ALT3\0\u{1}TRANSPORT_MQTT\0\u{1}TRANSPORT_MULTICAST_UDP\0\u{1}TRANSPORT_API\0") } extension NodeInfo: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { public static let protoMessageName: String = _protobuf_package + ".NodeInfo" - public static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ - 1: .same(proto: "num"), - 2: .same(proto: "user"), - 3: .same(proto: "position"), - 4: .same(proto: "snr"), - 5: .standard(proto: "last_heard"), - 6: .standard(proto: "device_metrics"), - 7: .same(proto: "channel"), - 8: .standard(proto: "via_mqtt"), - 9: .standard(proto: "hops_away"), - 10: .standard(proto: "is_favorite"), - 11: .standard(proto: "is_ignored"), - 12: .standard(proto: "is_key_manually_verified"), - ] + 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") fileprivate class _StorageClass { var _num: UInt32 = 0 @@ -5412,15 +4997,11 @@ extension NodeInfo: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationB var _isIgnored: Bool = false var _isKeyManuallyVerified: Bool = false - #if swift(>=5.10) // 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. // This will force a copy to be made of this reference when the first mutation occurs; // hence, it is safe to mark this as `nonisolated(unsafe)`. static nonisolated(unsafe) let defaultInstance = _StorageClass() - #else - static let defaultInstance = _StorageClass() - #endif private init() {} @@ -5488,7 +5069,7 @@ extension NodeInfo: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationB try { if let v = _storage._position { try visitor.visitSingularMessageField(value: v, fieldNumber: 3) } }() - if _storage._snr != 0 { + if _storage._snr.bitPattern != 0 { try visitor.visitSingularFloatField(value: _storage._snr, fieldNumber: 4) } if _storage._lastHeard != 0 { @@ -5547,15 +5128,7 @@ extension NodeInfo: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationB extension MyNodeInfo: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { public static let protoMessageName: String = _protobuf_package + ".MyNodeInfo" - public static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ - 1: .standard(proto: "my_node_num"), - 8: .standard(proto: "reboot_count"), - 11: .standard(proto: "min_app_version"), - 12: .standard(proto: "device_id"), - 13: .standard(proto: "pio_env"), - 14: .standard(proto: "firmware_edition"), - 15: .standard(proto: "nodedb_count"), - ] + public static let _protobuf_nameMap = SwiftProtobuf._NameMap(bytecode: "\0\u{3}my_node_num\0\u{4}\u{7}reboot_count\0\u{4}\u{3}min_app_version\0\u{3}device_id\0\u{3}pio_env\0\u{3}firmware_edition\0\u{3}nodedb_count\0") public mutating func decodeMessage(decoder: inout D) throws { while let fieldNumber = try decoder.nextFieldNumber() { @@ -5615,12 +5188,7 @@ extension MyNodeInfo: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementatio extension LogRecord: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { public static let protoMessageName: String = _protobuf_package + ".LogRecord" - public static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ - 1: .same(proto: "message"), - 2: .same(proto: "time"), - 3: .same(proto: "source"), - 4: .same(proto: "level"), - ] + public static let _protobuf_nameMap = SwiftProtobuf._NameMap(bytecode: "\0\u{1}message\0\u{1}time\0\u{1}source\0\u{1}level\0") public mutating func decodeMessage(decoder: inout D) throws { while let fieldNumber = try decoder.nextFieldNumber() { @@ -5664,25 +5232,12 @@ extension LogRecord: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementation } extension LogRecord.Level: SwiftProtobuf._ProtoNameProviding { - public static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ - 0: .same(proto: "UNSET"), - 5: .same(proto: "TRACE"), - 10: .same(proto: "DEBUG"), - 20: .same(proto: "INFO"), - 30: .same(proto: "WARNING"), - 40: .same(proto: "ERROR"), - 50: .same(proto: "CRITICAL"), - ] + public static let _protobuf_nameMap = SwiftProtobuf._NameMap(bytecode: "\0\u{2}\0UNSET\0\u{2}\u{5}TRACE\0\u{2}\u{5}DEBUG\0\u{2}\u{a}INFO\0\u{2}\u{a}WARNING\0\u{2}\u{a}ERROR\0\u{2}\u{a}CRITICAL\0") } extension QueueStatus: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { public static let protoMessageName: String = _protobuf_package + ".QueueStatus" - public static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ - 1: .same(proto: "res"), - 2: .same(proto: "free"), - 3: .same(proto: "maxlen"), - 4: .standard(proto: "mesh_packet_id"), - ] + public static let _protobuf_nameMap = SwiftProtobuf._NameMap(bytecode: "\0\u{1}res\0\u{1}free\0\u{1}maxlen\0\u{3}mesh_packet_id\0") public mutating func decodeMessage(decoder: inout D) throws { while let fieldNumber = try decoder.nextFieldNumber() { @@ -5727,25 +5282,7 @@ extension QueueStatus: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementati extension FromRadio: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { public static let protoMessageName: String = _protobuf_package + ".FromRadio" - public static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ - 1: .same(proto: "id"), - 2: .same(proto: "packet"), - 3: .standard(proto: "my_info"), - 4: .standard(proto: "node_info"), - 5: .same(proto: "config"), - 6: .standard(proto: "log_record"), - 7: .standard(proto: "config_complete_id"), - 8: .same(proto: "rebooted"), - 9: .same(proto: "moduleConfig"), - 10: .same(proto: "channel"), - 11: .same(proto: "queueStatus"), - 12: .same(proto: "xmodemPacket"), - 13: .same(proto: "metadata"), - 14: .same(proto: "mqttClientProxyMessage"), - 15: .same(proto: "fileInfo"), - 16: .same(proto: "clientNotification"), - 17: .same(proto: "deviceuiConfig"), - ] + public static let _protobuf_nameMap = SwiftProtobuf._NameMap(bytecode: "\0\u{1}id\0\u{1}packet\0\u{3}my_info\0\u{3}node_info\0\u{1}config\0\u{3}log_record\0\u{3}config_complete_id\0\u{1}rebooted\0\u{1}moduleConfig\0\u{1}channel\0\u{1}queueStatus\0\u{1}xmodemPacket\0\u{1}metadata\0\u{1}mqttClientProxyMessage\0\u{1}fileInfo\0\u{1}clientNotification\0\u{1}deviceuiConfig\0") public mutating func decodeMessage(decoder: inout D) throws { while let fieldNumber = try decoder.nextFieldNumber() { @@ -6045,17 +5582,7 @@ extension FromRadio: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementation extension ClientNotification: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { public static let protoMessageName: String = _protobuf_package + ".ClientNotification" - public static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ - 1: .standard(proto: "reply_id"), - 2: .same(proto: "time"), - 3: .same(proto: "level"), - 4: .same(proto: "message"), - 11: .standard(proto: "key_verification_number_inform"), - 12: .standard(proto: "key_verification_number_request"), - 13: .standard(proto: "key_verification_final"), - 14: .standard(proto: "duplicated_public_key"), - 15: .standard(proto: "low_entropy_key"), - ] + public static let _protobuf_nameMap = SwiftProtobuf._NameMap(bytecode: "\0\u{3}reply_id\0\u{1}time\0\u{1}level\0\u{1}message\0\u{4}\u{7}key_verification_number_inform\0\u{3}key_verification_number_request\0\u{3}key_verification_final\0\u{3}duplicated_public_key\0\u{3}low_entropy_key\0") public mutating func decodeMessage(decoder: inout D) throws { while let fieldNumber = try decoder.nextFieldNumber() { @@ -6193,11 +5720,7 @@ extension ClientNotification: SwiftProtobuf.Message, SwiftProtobuf._MessageImple extension KeyVerificationNumberInform: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { public static let protoMessageName: String = _protobuf_package + ".KeyVerificationNumberInform" - public static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ - 1: .same(proto: "nonce"), - 2: .standard(proto: "remote_longname"), - 3: .standard(proto: "security_number"), - ] + public static let _protobuf_nameMap = SwiftProtobuf._NameMap(bytecode: "\0\u{1}nonce\0\u{3}remote_longname\0\u{3}security_number\0") public mutating func decodeMessage(decoder: inout D) throws { while let fieldNumber = try decoder.nextFieldNumber() { @@ -6237,10 +5760,7 @@ extension KeyVerificationNumberInform: SwiftProtobuf.Message, SwiftProtobuf._Mes extension KeyVerificationNumberRequest: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { public static let protoMessageName: String = _protobuf_package + ".KeyVerificationNumberRequest" - public static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ - 1: .same(proto: "nonce"), - 2: .standard(proto: "remote_longname"), - ] + public static let _protobuf_nameMap = SwiftProtobuf._NameMap(bytecode: "\0\u{1}nonce\0\u{3}remote_longname\0") public mutating func decodeMessage(decoder: inout D) throws { while let fieldNumber = try decoder.nextFieldNumber() { @@ -6275,12 +5795,7 @@ extension KeyVerificationNumberRequest: SwiftProtobuf.Message, SwiftProtobuf._Me extension KeyVerificationFinal: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { public static let protoMessageName: String = _protobuf_package + ".KeyVerificationFinal" - public static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ - 1: .same(proto: "nonce"), - 2: .standard(proto: "remote_longname"), - 3: .same(proto: "isSender"), - 4: .standard(proto: "verification_characters"), - ] + public static let _protobuf_nameMap = SwiftProtobuf._NameMap(bytecode: "\0\u{1}nonce\0\u{3}remote_longname\0\u{1}isSender\0\u{3}verification_characters\0") public mutating func decodeMessage(decoder: inout D) throws { while let fieldNumber = try decoder.nextFieldNumber() { @@ -6328,8 +5843,8 @@ extension DuplicatedPublicKey: SwiftProtobuf.Message, SwiftProtobuf._MessageImpl public static let _protobuf_nameMap = SwiftProtobuf._NameMap() public mutating func decodeMessage(decoder: inout D) throws { - while let _ = try decoder.nextFieldNumber() { - } + // Load everything into unknown fields + while try decoder.nextFieldNumber() != nil {} } public func traverse(visitor: inout V) throws { @@ -6347,8 +5862,8 @@ extension LowEntropyKey: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementa public static let _protobuf_nameMap = SwiftProtobuf._NameMap() public mutating func decodeMessage(decoder: inout D) throws { - while let _ = try decoder.nextFieldNumber() { - } + // Load everything into unknown fields + while try decoder.nextFieldNumber() != nil {} } public func traverse(visitor: inout V) throws { @@ -6363,10 +5878,7 @@ extension LowEntropyKey: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementa extension FileInfo: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { public static let protoMessageName: String = _protobuf_package + ".FileInfo" - public static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ - 1: .standard(proto: "file_name"), - 2: .standard(proto: "size_bytes"), - ] + public static let _protobuf_nameMap = SwiftProtobuf._NameMap(bytecode: "\0\u{3}file_name\0\u{3}size_bytes\0") public mutating func decodeMessage(decoder: inout D) throws { while let fieldNumber = try decoder.nextFieldNumber() { @@ -6401,14 +5913,7 @@ extension FileInfo: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationB extension ToRadio: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { public static let protoMessageName: String = _protobuf_package + ".ToRadio" - public static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ - 1: .same(proto: "packet"), - 3: .standard(proto: "want_config_id"), - 4: .same(proto: "disconnect"), - 5: .same(proto: "xmodemPacket"), - 6: .same(proto: "mqttClientProxyMessage"), - 7: .same(proto: "heartbeat"), - ] + public static let _protobuf_nameMap = SwiftProtobuf._NameMap(bytecode: "\0\u{1}packet\0\u{4}\u{2}want_config_id\0\u{1}disconnect\0\u{1}xmodemPacket\0\u{1}mqttClientProxyMessage\0\u{1}heartbeat\0") public mutating func decodeMessage(decoder: inout D) throws { while let fieldNumber = try decoder.nextFieldNumber() { @@ -6533,10 +6038,7 @@ extension ToRadio: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBa extension Compressed: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { public static let protoMessageName: String = _protobuf_package + ".Compressed" - public static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ - 1: .same(proto: "portnum"), - 2: .same(proto: "data"), - ] + public static let _protobuf_nameMap = SwiftProtobuf._NameMap(bytecode: "\0\u{1}portnum\0\u{1}data\0") public mutating func decodeMessage(decoder: inout D) throws { while let fieldNumber = try decoder.nextFieldNumber() { @@ -6571,12 +6073,7 @@ extension Compressed: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementatio extension NeighborInfo: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { public static let protoMessageName: String = _protobuf_package + ".NeighborInfo" - public static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ - 1: .standard(proto: "node_id"), - 2: .standard(proto: "last_sent_by_id"), - 3: .standard(proto: "node_broadcast_interval_secs"), - 4: .same(proto: "neighbors"), - ] + public static let _protobuf_nameMap = SwiftProtobuf._NameMap(bytecode: "\0\u{3}node_id\0\u{3}last_sent_by_id\0\u{3}node_broadcast_interval_secs\0\u{1}neighbors\0") public mutating func decodeMessage(decoder: inout D) throws { while let fieldNumber = try decoder.nextFieldNumber() { @@ -6621,12 +6118,7 @@ extension NeighborInfo: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementat extension Neighbor: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { public static let protoMessageName: String = _protobuf_package + ".Neighbor" - public static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ - 1: .standard(proto: "node_id"), - 2: .same(proto: "snr"), - 3: .standard(proto: "last_rx_time"), - 4: .standard(proto: "node_broadcast_interval_secs"), - ] + public static let _protobuf_nameMap = SwiftProtobuf._NameMap(bytecode: "\0\u{3}node_id\0\u{1}snr\0\u{3}last_rx_time\0\u{3}node_broadcast_interval_secs\0") public mutating func decodeMessage(decoder: inout D) throws { while let fieldNumber = try decoder.nextFieldNumber() { @@ -6647,7 +6139,7 @@ extension Neighbor: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationB if self.nodeID != 0 { try visitor.visitSingularUInt32Field(value: self.nodeID, fieldNumber: 1) } - if self.snr != 0 { + if self.snr.bitPattern != 0 { try visitor.visitSingularFloatField(value: self.snr, fieldNumber: 2) } if self.lastRxTime != 0 { @@ -6671,20 +6163,7 @@ extension Neighbor: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationB extension DeviceMetadata: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { public static let protoMessageName: String = _protobuf_package + ".DeviceMetadata" - public static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ - 1: .standard(proto: "firmware_version"), - 2: .standard(proto: "device_state_version"), - 3: .same(proto: "canShutdown"), - 4: .same(proto: "hasWifi"), - 5: .same(proto: "hasBluetooth"), - 6: .same(proto: "hasEthernet"), - 7: .same(proto: "role"), - 8: .standard(proto: "position_flags"), - 9: .standard(proto: "hw_model"), - 10: .same(proto: "hasRemoteHardware"), - 11: .same(proto: "hasPKC"), - 12: .standard(proto: "excluded_modules"), - ] + public static let _protobuf_nameMap = SwiftProtobuf._NameMap(bytecode: "\0\u{3}firmware_version\0\u{3}device_state_version\0\u{1}canShutdown\0\u{1}hasWifi\0\u{1}hasBluetooth\0\u{1}hasEthernet\0\u{1}role\0\u{3}position_flags\0\u{3}hw_model\0\u{1}hasRemoteHardware\0\u{1}hasPKC\0\u{3}excluded_modules\0") public mutating func decodeMessage(decoder: inout D) throws { while let fieldNumber = try decoder.nextFieldNumber() { @@ -6769,9 +6248,7 @@ extension DeviceMetadata: SwiftProtobuf.Message, SwiftProtobuf._MessageImplement extension Heartbeat: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { public static let protoMessageName: String = _protobuf_package + ".Heartbeat" - public static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ - 1: .same(proto: "nonce"), - ] + public static let _protobuf_nameMap = SwiftProtobuf._NameMap(bytecode: "\0\u{1}nonce\0") public mutating func decodeMessage(decoder: inout D) throws { while let fieldNumber = try decoder.nextFieldNumber() { @@ -6801,10 +6278,7 @@ extension Heartbeat: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementation extension NodeRemoteHardwarePin: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { public static let protoMessageName: String = _protobuf_package + ".NodeRemoteHardwarePin" - public static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ - 1: .standard(proto: "node_num"), - 2: .same(proto: "pin"), - ] + public static let _protobuf_nameMap = SwiftProtobuf._NameMap(bytecode: "\0\u{3}node_num\0\u{1}pin\0") public mutating func decodeMessage(decoder: inout D) throws { while let fieldNumber = try decoder.nextFieldNumber() { @@ -6843,12 +6317,7 @@ extension NodeRemoteHardwarePin: SwiftProtobuf.Message, SwiftProtobuf._MessageIm extension ChunkedPayload: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { public static let protoMessageName: String = _protobuf_package + ".ChunkedPayload" - public static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ - 1: .standard(proto: "payload_id"), - 2: .standard(proto: "chunk_count"), - 3: .standard(proto: "chunk_index"), - 4: .standard(proto: "payload_chunk"), - ] + public static let _protobuf_nameMap = SwiftProtobuf._NameMap(bytecode: "\0\u{3}payload_id\0\u{3}chunk_count\0\u{3}chunk_index\0\u{3}payload_chunk\0") public mutating func decodeMessage(decoder: inout D) throws { while let fieldNumber = try decoder.nextFieldNumber() { @@ -6893,9 +6362,7 @@ extension ChunkedPayload: SwiftProtobuf.Message, SwiftProtobuf._MessageImplement extension resend_chunks: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { public static let protoMessageName: String = _protobuf_package + ".resend_chunks" - public static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ - 1: .same(proto: "chunks"), - ] + public static let _protobuf_nameMap = SwiftProtobuf._NameMap(bytecode: "\0\u{1}chunks\0") public mutating func decodeMessage(decoder: inout D) throws { while let fieldNumber = try decoder.nextFieldNumber() { @@ -6925,12 +6392,7 @@ extension resend_chunks: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementa extension ChunkedPayloadResponse: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { public static let protoMessageName: String = _protobuf_package + ".ChunkedPayloadResponse" - public static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ - 1: .standard(proto: "payload_id"), - 2: .standard(proto: "request_transfer"), - 3: .standard(proto: "accept_transfer"), - 4: .standard(proto: "resend_chunks"), - ] + public static let _protobuf_nameMap = SwiftProtobuf._NameMap(bytecode: "\0\u{3}payload_id\0\u{3}request_transfer\0\u{3}accept_transfer\0\u{3}resend_chunks\0") public mutating func decodeMessage(decoder: inout D) throws { while let fieldNumber = try decoder.nextFieldNumber() { diff --git a/MeshtasticProtobufs/Sources/meshtastic/module_config.pb.swift b/MeshtasticProtobufs/Sources/meshtastic/module_config.pb.swift index 5bf37457..1bff36a2 100644 --- a/MeshtasticProtobufs/Sources/meshtastic/module_config.pb.swift +++ b/MeshtasticProtobufs/Sources/meshtastic/module_config.pb.swift @@ -1,5 +1,6 @@ // DO NOT EDIT. // swift-format-ignore-file +// swiftlint:disable all // // Generated by the Swift generator plugin for the protocol buffer compiler. // Source: meshtastic/module_config.proto @@ -7,7 +8,6 @@ // For information on using the generated types, please see the documentation: // https://github.com/apple/swift-protobuf/ -import Foundation import SwiftProtobuf // If the compiler emits an error on this type, it is because this file @@ -20,7 +20,7 @@ fileprivate struct _GeneratedWithProtocGenSwiftVersion: SwiftProtobuf.ProtobufAP typealias Version = _2 } -public enum RemoteHardwarePinType: SwiftProtobuf.Enum { +public enum RemoteHardwarePinType: SwiftProtobuf.Enum, Swift.CaseIterable { public typealias RawValue = Int /// @@ -58,24 +58,18 @@ public enum RemoteHardwarePinType: SwiftProtobuf.Enum { } } -} - -#if swift(>=4.2) - -extension RemoteHardwarePinType: CaseIterable { // The compiler won't synthesize support with the UNRECOGNIZED case. public static let allCases: [RemoteHardwarePinType] = [ .unknown, .digitalRead, .digitalWrite, ] -} -#endif // swift(>=4.2) +} /// /// Module Config -public struct ModuleConfig { +public struct ModuleConfig: 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. @@ -218,7 +212,7 @@ public struct ModuleConfig { /// /// TODO: REPLACE - public enum OneOf_PayloadVariant: Equatable { + public enum OneOf_PayloadVariant: Equatable, Sendable { /// /// TODO: REPLACE case mqtt(ModuleConfig.MQTTConfig) @@ -259,73 +253,11 @@ public struct ModuleConfig { /// TODO: REPLACE case paxcounter(ModuleConfig.PaxcounterConfig) - #if !swift(>=4.1) - public static func ==(lhs: ModuleConfig.OneOf_PayloadVariant, rhs: ModuleConfig.OneOf_PayloadVariant) -> Bool { - // 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 (lhs, rhs) { - case (.mqtt, .mqtt): return { - guard case .mqtt(let l) = lhs, case .mqtt(let r) = rhs else { preconditionFailure() } - return l == r - }() - case (.serial, .serial): return { - guard case .serial(let l) = lhs, case .serial(let r) = rhs else { preconditionFailure() } - return l == r - }() - case (.externalNotification, .externalNotification): return { - guard case .externalNotification(let l) = lhs, case .externalNotification(let r) = rhs else { preconditionFailure() } - return l == r - }() - case (.storeForward, .storeForward): return { - guard case .storeForward(let l) = lhs, case .storeForward(let r) = rhs else { preconditionFailure() } - return l == r - }() - case (.rangeTest, .rangeTest): return { - guard case .rangeTest(let l) = lhs, case .rangeTest(let r) = rhs else { preconditionFailure() } - return l == r - }() - case (.telemetry, .telemetry): return { - guard case .telemetry(let l) = lhs, case .telemetry(let r) = rhs else { preconditionFailure() } - return l == r - }() - case (.cannedMessage, .cannedMessage): return { - guard case .cannedMessage(let l) = lhs, case .cannedMessage(let r) = rhs else { preconditionFailure() } - return l == r - }() - case (.audio, .audio): return { - guard case .audio(let l) = lhs, case .audio(let r) = rhs else { preconditionFailure() } - return l == r - }() - case (.remoteHardware, .remoteHardware): return { - guard case .remoteHardware(let l) = lhs, case .remoteHardware(let r) = rhs else { preconditionFailure() } - return l == r - }() - case (.neighborInfo, .neighborInfo): return { - guard case .neighborInfo(let l) = lhs, case .neighborInfo(let r) = rhs else { preconditionFailure() } - return l == r - }() - case (.ambientLighting, .ambientLighting): return { - guard case .ambientLighting(let l) = lhs, case .ambientLighting(let r) = rhs else { preconditionFailure() } - return l == r - }() - case (.detectionSensor, .detectionSensor): return { - guard case .detectionSensor(let l) = lhs, case .detectionSensor(let r) = rhs else { preconditionFailure() } - return l == r - }() - case (.paxcounter, .paxcounter): return { - guard case .paxcounter(let l) = lhs, case .paxcounter(let r) = rhs else { preconditionFailure() } - return l == r - }() - default: return false - } - } - #endif } /// /// MQTT Client Config - public struct MQTTConfig { + public struct MQTTConfig: 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. @@ -400,7 +332,7 @@ public struct ModuleConfig { /// /// Settings for reporting unencrypted information about our node to a map via MQTT - public struct MapReportSettings { + public struct MapReportSettings: 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. @@ -424,7 +356,7 @@ public struct ModuleConfig { /// /// RemoteHardwareModule Config - public struct RemoteHardwareConfig { + public struct RemoteHardwareConfig: 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. @@ -448,7 +380,7 @@ public struct ModuleConfig { /// /// NeighborInfoModule Config - public struct NeighborInfoConfig { + public struct NeighborInfoConfig: 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. @@ -474,7 +406,7 @@ public struct ModuleConfig { /// /// Detection Sensor Module Config - public struct DetectionSensorConfig { + public struct DetectionSensorConfig: 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. @@ -521,7 +453,7 @@ public struct ModuleConfig { public var unknownFields = SwiftProtobuf.UnknownStorage() - public enum TriggerType: SwiftProtobuf.Enum { + public enum TriggerType: SwiftProtobuf.Enum, Swift.CaseIterable { public typealias RawValue = Int /// Event is triggered if pin is low @@ -573,6 +505,16 @@ public struct ModuleConfig { } } + // The compiler won't synthesize support with the UNRECOGNIZED case. + public static let allCases: [ModuleConfig.DetectionSensorConfig.TriggerType] = [ + .logicLow, + .logicHigh, + .fallingEdge, + .risingEdge, + .eitherEdgeActiveLow, + .eitherEdgeActiveHigh, + ] + } public init() {} @@ -580,7 +522,7 @@ public struct ModuleConfig { /// /// Audio Config for codec2 voice - public struct AudioConfig { + public struct AudioConfig: 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. @@ -617,7 +559,7 @@ public struct ModuleConfig { /// /// Baudrate for codec2 voice - public enum Audio_Baud: SwiftProtobuf.Enum { + public enum Audio_Baud: SwiftProtobuf.Enum, Swift.CaseIterable { public typealias RawValue = Int case codec2Default // = 0 case codec23200 // = 1 @@ -664,6 +606,19 @@ public struct ModuleConfig { } } + // The compiler won't synthesize support with the UNRECOGNIZED case. + public static let allCases: [ModuleConfig.AudioConfig.Audio_Baud] = [ + .codec2Default, + .codec23200, + .codec22400, + .codec21600, + .codec21400, + .codec21300, + .codec21200, + .codec2700, + .codec2700B, + ] + } public init() {} @@ -671,7 +626,7 @@ public struct ModuleConfig { /// /// Config for the Paxcounter Module - public struct PaxcounterConfig { + public struct PaxcounterConfig: 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. @@ -697,7 +652,7 @@ public struct ModuleConfig { /// /// Serial Config - public struct SerialConfig { + public struct SerialConfig: 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. @@ -740,7 +695,7 @@ public struct ModuleConfig { /// /// TODO: REPLACE - public enum Serial_Baud: SwiftProtobuf.Enum { + public enum Serial_Baud: SwiftProtobuf.Enum, Swift.CaseIterable { public typealias RawValue = Int case baudDefault // = 0 case baud110 // = 1 @@ -808,11 +763,31 @@ public struct ModuleConfig { } } + // The compiler won't synthesize support with the UNRECOGNIZED case. + public static let allCases: [ModuleConfig.SerialConfig.Serial_Baud] = [ + .baudDefault, + .baud110, + .baud300, + .baud600, + .baud1200, + .baud2400, + .baud4800, + .baud9600, + .baud19200, + .baud38400, + .baud57600, + .baud115200, + .baud230400, + .baud460800, + .baud576000, + .baud921600, + ] + } /// /// TODO: REPLACE - public enum Serial_Mode: SwiftProtobuf.Enum { + public enum Serial_Mode: SwiftProtobuf.Enum, Swift.CaseIterable { public typealias RawValue = Int case `default` // = 0 case simple // = 1 @@ -869,6 +844,19 @@ public struct ModuleConfig { } } + // The compiler won't synthesize support with the UNRECOGNIZED case. + public static let allCases: [ModuleConfig.SerialConfig.Serial_Mode] = [ + .default, + .simple, + .proto, + .textmsg, + .nmea, + .caltopo, + .ws85, + .veDirect, + .msConfig, + ] + } public init() {} @@ -876,7 +864,7 @@ public struct ModuleConfig { /// /// External Notifications Config - public struct ExternalNotificationConfig { + public struct ExternalNotificationConfig: 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. @@ -959,7 +947,7 @@ public struct ModuleConfig { /// /// Store and Forward Module Config - public struct StoreForwardConfig { + public struct StoreForwardConfig: 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. @@ -995,7 +983,7 @@ public struct ModuleConfig { /// /// Preferences for the RangeTestModule - public struct RangeTestConfig { + public struct RangeTestConfig: 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. @@ -1025,7 +1013,7 @@ public struct ModuleConfig { /// /// Configuration for both device and environment metrics - public struct TelemetryConfig { + public struct TelemetryConfig: 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. @@ -1099,7 +1087,7 @@ public struct ModuleConfig { /// /// Canned Messages Module Config - public struct CannedMessageConfig { + public struct CannedMessageConfig: 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. @@ -1138,11 +1126,15 @@ public struct ModuleConfig { /// /// Enable/disable CannedMessageModule. + /// + /// NOTE: This field was marked as deprecated in the .proto file. public var enabled: Bool = false /// /// Input event origin accepted by the canned message module. /// Can be e.g. "rotEnc1", "upDownEnc1", "scanAndSelect", "cardkb", "serialkb", or keyword "_any" + /// + /// NOTE: This field was marked as deprecated in the .proto file. public var allowInputSource: String = String() /// @@ -1154,7 +1146,7 @@ public struct ModuleConfig { /// /// TODO: REPLACE - public enum InputEventChar: SwiftProtobuf.Enum { + public enum InputEventChar: SwiftProtobuf.Enum, Swift.CaseIterable { public typealias RawValue = Int /// @@ -1222,6 +1214,18 @@ public struct ModuleConfig { } } + // The compiler won't synthesize support with the UNRECOGNIZED case. + public static let allCases: [ModuleConfig.CannedMessageConfig.InputEventChar] = [ + .none, + .up, + .down, + .left, + .right, + .select, + .back, + .cancel, + ] + } public init() {} @@ -1230,7 +1234,7 @@ public struct ModuleConfig { /// ///Ambient Lighting Module - Settings for control of onboard LEDs to allow users to adjust the brightness levels and respective color levels. ///Initially created for the RAK14001 RGB LED module. - public struct AmbientLightingConfig { + public struct AmbientLightingConfig: 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. @@ -1263,91 +1267,9 @@ public struct ModuleConfig { public init() {} } -#if swift(>=4.2) - -extension ModuleConfig.DetectionSensorConfig.TriggerType: CaseIterable { - // The compiler won't synthesize support with the UNRECOGNIZED case. - public static let allCases: [ModuleConfig.DetectionSensorConfig.TriggerType] = [ - .logicLow, - .logicHigh, - .fallingEdge, - .risingEdge, - .eitherEdgeActiveLow, - .eitherEdgeActiveHigh, - ] -} - -extension ModuleConfig.AudioConfig.Audio_Baud: CaseIterable { - // The compiler won't synthesize support with the UNRECOGNIZED case. - public static let allCases: [ModuleConfig.AudioConfig.Audio_Baud] = [ - .codec2Default, - .codec23200, - .codec22400, - .codec21600, - .codec21400, - .codec21300, - .codec21200, - .codec2700, - .codec2700B, - ] -} - -extension ModuleConfig.SerialConfig.Serial_Baud: CaseIterable { - // The compiler won't synthesize support with the UNRECOGNIZED case. - public static let allCases: [ModuleConfig.SerialConfig.Serial_Baud] = [ - .baudDefault, - .baud110, - .baud300, - .baud600, - .baud1200, - .baud2400, - .baud4800, - .baud9600, - .baud19200, - .baud38400, - .baud57600, - .baud115200, - .baud230400, - .baud460800, - .baud576000, - .baud921600, - ] -} - -extension ModuleConfig.SerialConfig.Serial_Mode: CaseIterable { - // The compiler won't synthesize support with the UNRECOGNIZED case. - public static let allCases: [ModuleConfig.SerialConfig.Serial_Mode] = [ - .default, - .simple, - .proto, - .textmsg, - .nmea, - .caltopo, - .ws85, - .veDirect, - .msConfig, - ] -} - -extension ModuleConfig.CannedMessageConfig.InputEventChar: CaseIterable { - // The compiler won't synthesize support with the UNRECOGNIZED case. - public static let allCases: [ModuleConfig.CannedMessageConfig.InputEventChar] = [ - .none, - .up, - .down, - .left, - .right, - .select, - .back, - .cancel, - ] -} - -#endif // swift(>=4.2) - /// /// A GPIO pin definition for remote hardware module -public struct RemoteHardwarePin { +public struct RemoteHardwarePin: 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. @@ -1369,61 +1291,17 @@ public struct RemoteHardwarePin { public init() {} } -#if swift(>=5.5) && canImport(_Concurrency) -extension RemoteHardwarePinType: @unchecked Sendable {} -extension ModuleConfig: @unchecked Sendable {} -extension ModuleConfig.OneOf_PayloadVariant: @unchecked Sendable {} -extension ModuleConfig.MQTTConfig: @unchecked Sendable {} -extension ModuleConfig.MapReportSettings: @unchecked Sendable {} -extension ModuleConfig.RemoteHardwareConfig: @unchecked Sendable {} -extension ModuleConfig.NeighborInfoConfig: @unchecked Sendable {} -extension ModuleConfig.DetectionSensorConfig: @unchecked Sendable {} -extension ModuleConfig.DetectionSensorConfig.TriggerType: @unchecked Sendable {} -extension ModuleConfig.AudioConfig: @unchecked Sendable {} -extension ModuleConfig.AudioConfig.Audio_Baud: @unchecked Sendable {} -extension ModuleConfig.PaxcounterConfig: @unchecked Sendable {} -extension ModuleConfig.SerialConfig: @unchecked Sendable {} -extension ModuleConfig.SerialConfig.Serial_Baud: @unchecked Sendable {} -extension ModuleConfig.SerialConfig.Serial_Mode: @unchecked Sendable {} -extension ModuleConfig.ExternalNotificationConfig: @unchecked Sendable {} -extension ModuleConfig.StoreForwardConfig: @unchecked Sendable {} -extension ModuleConfig.RangeTestConfig: @unchecked Sendable {} -extension ModuleConfig.TelemetryConfig: @unchecked Sendable {} -extension ModuleConfig.CannedMessageConfig: @unchecked Sendable {} -extension ModuleConfig.CannedMessageConfig.InputEventChar: @unchecked Sendable {} -extension ModuleConfig.AmbientLightingConfig: @unchecked Sendable {} -extension RemoteHardwarePin: @unchecked Sendable {} -#endif // swift(>=5.5) && canImport(_Concurrency) - // MARK: - Code below here is support for the SwiftProtobuf runtime. fileprivate let _protobuf_package = "meshtastic" extension RemoteHardwarePinType: SwiftProtobuf._ProtoNameProviding { - public static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ - 0: .same(proto: "UNKNOWN"), - 1: .same(proto: "DIGITAL_READ"), - 2: .same(proto: "DIGITAL_WRITE"), - ] + public static let _protobuf_nameMap = SwiftProtobuf._NameMap(bytecode: "\0\u{2}\0UNKNOWN\0\u{1}DIGITAL_READ\0\u{1}DIGITAL_WRITE\0") } extension ModuleConfig: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { public static let protoMessageName: String = _protobuf_package + ".ModuleConfig" - public static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ - 1: .same(proto: "mqtt"), - 2: .same(proto: "serial"), - 3: .standard(proto: "external_notification"), - 4: .standard(proto: "store_forward"), - 5: .standard(proto: "range_test"), - 6: .same(proto: "telemetry"), - 7: .standard(proto: "canned_message"), - 8: .same(proto: "audio"), - 9: .standard(proto: "remote_hardware"), - 10: .standard(proto: "neighbor_info"), - 11: .standard(proto: "ambient_lighting"), - 12: .standard(proto: "detection_sensor"), - 13: .same(proto: "paxcounter"), - ] + public static let _protobuf_nameMap = SwiftProtobuf._NameMap(bytecode: "\0\u{1}mqtt\0\u{1}serial\0\u{3}external_notification\0\u{3}store_forward\0\u{3}range_test\0\u{1}telemetry\0\u{3}canned_message\0\u{1}audio\0\u{3}remote_hardware\0\u{3}neighbor_info\0\u{3}ambient_lighting\0\u{3}detection_sensor\0\u{1}paxcounter\0") public mutating func decodeMessage(decoder: inout D) throws { while let fieldNumber = try decoder.nextFieldNumber() { @@ -1677,19 +1555,7 @@ extension ModuleConfig: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementat extension ModuleConfig.MQTTConfig: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { public static let protoMessageName: String = ModuleConfig.protoMessageName + ".MQTTConfig" - public static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ - 1: .same(proto: "enabled"), - 2: .same(proto: "address"), - 3: .same(proto: "username"), - 4: .same(proto: "password"), - 5: .standard(proto: "encryption_enabled"), - 6: .standard(proto: "json_enabled"), - 7: .standard(proto: "tls_enabled"), - 8: .same(proto: "root"), - 9: .standard(proto: "proxy_to_client_enabled"), - 10: .standard(proto: "map_reporting_enabled"), - 11: .standard(proto: "map_report_settings"), - ] + public static let _protobuf_nameMap = SwiftProtobuf._NameMap(bytecode: "\0\u{1}enabled\0\u{1}address\0\u{1}username\0\u{1}password\0\u{3}encryption_enabled\0\u{3}json_enabled\0\u{3}tls_enabled\0\u{1}root\0\u{3}proxy_to_client_enabled\0\u{3}map_reporting_enabled\0\u{3}map_report_settings\0") public mutating func decodeMessage(decoder: inout D) throws { while let fieldNumber = try decoder.nextFieldNumber() { @@ -1773,11 +1639,7 @@ extension ModuleConfig.MQTTConfig: SwiftProtobuf.Message, SwiftProtobuf._Message extension ModuleConfig.MapReportSettings: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { public static let protoMessageName: String = ModuleConfig.protoMessageName + ".MapReportSettings" - public static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ - 1: .standard(proto: "publish_interval_secs"), - 2: .standard(proto: "position_precision"), - 3: .standard(proto: "should_report_location"), - ] + public static let _protobuf_nameMap = SwiftProtobuf._NameMap(bytecode: "\0\u{3}publish_interval_secs\0\u{3}position_precision\0\u{3}should_report_location\0") public mutating func decodeMessage(decoder: inout D) throws { while let fieldNumber = try decoder.nextFieldNumber() { @@ -1817,11 +1679,7 @@ extension ModuleConfig.MapReportSettings: SwiftProtobuf.Message, SwiftProtobuf._ extension ModuleConfig.RemoteHardwareConfig: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { public static let protoMessageName: String = ModuleConfig.protoMessageName + ".RemoteHardwareConfig" - public static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ - 1: .same(proto: "enabled"), - 2: .standard(proto: "allow_undefined_pin_access"), - 3: .standard(proto: "available_pins"), - ] + public static let _protobuf_nameMap = SwiftProtobuf._NameMap(bytecode: "\0\u{1}enabled\0\u{3}allow_undefined_pin_access\0\u{3}available_pins\0") public mutating func decodeMessage(decoder: inout D) throws { while let fieldNumber = try decoder.nextFieldNumber() { @@ -1861,11 +1719,7 @@ extension ModuleConfig.RemoteHardwareConfig: SwiftProtobuf.Message, SwiftProtobu extension ModuleConfig.NeighborInfoConfig: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { public static let protoMessageName: String = ModuleConfig.protoMessageName + ".NeighborInfoConfig" - public static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ - 1: .same(proto: "enabled"), - 2: .standard(proto: "update_interval"), - 3: .standard(proto: "transmit_over_lora"), - ] + public static let _protobuf_nameMap = SwiftProtobuf._NameMap(bytecode: "\0\u{1}enabled\0\u{3}update_interval\0\u{3}transmit_over_lora\0") public mutating func decodeMessage(decoder: inout D) throws { while let fieldNumber = try decoder.nextFieldNumber() { @@ -1905,16 +1759,7 @@ extension ModuleConfig.NeighborInfoConfig: SwiftProtobuf.Message, SwiftProtobuf. extension ModuleConfig.DetectionSensorConfig: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { public static let protoMessageName: String = ModuleConfig.protoMessageName + ".DetectionSensorConfig" - public static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ - 1: .same(proto: "enabled"), - 2: .standard(proto: "minimum_broadcast_secs"), - 3: .standard(proto: "state_broadcast_secs"), - 4: .standard(proto: "send_bell"), - 5: .same(proto: "name"), - 6: .standard(proto: "monitor_pin"), - 7: .standard(proto: "detection_trigger_type"), - 8: .standard(proto: "use_pullup"), - ] + public static let _protobuf_nameMap = SwiftProtobuf._NameMap(bytecode: "\0\u{1}enabled\0\u{3}minimum_broadcast_secs\0\u{3}state_broadcast_secs\0\u{3}send_bell\0\u{1}name\0\u{3}monitor_pin\0\u{3}detection_trigger_type\0\u{3}use_pullup\0") public mutating func decodeMessage(decoder: inout D) throws { while let fieldNumber = try decoder.nextFieldNumber() { @@ -1978,27 +1823,12 @@ extension ModuleConfig.DetectionSensorConfig: SwiftProtobuf.Message, SwiftProtob } extension ModuleConfig.DetectionSensorConfig.TriggerType: SwiftProtobuf._ProtoNameProviding { - public static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ - 0: .same(proto: "LOGIC_LOW"), - 1: .same(proto: "LOGIC_HIGH"), - 2: .same(proto: "FALLING_EDGE"), - 3: .same(proto: "RISING_EDGE"), - 4: .same(proto: "EITHER_EDGE_ACTIVE_LOW"), - 5: .same(proto: "EITHER_EDGE_ACTIVE_HIGH"), - ] + public static let _protobuf_nameMap = SwiftProtobuf._NameMap(bytecode: "\0\u{2}\0LOGIC_LOW\0\u{1}LOGIC_HIGH\0\u{1}FALLING_EDGE\0\u{1}RISING_EDGE\0\u{1}EITHER_EDGE_ACTIVE_LOW\0\u{1}EITHER_EDGE_ACTIVE_HIGH\0") } extension ModuleConfig.AudioConfig: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { public static let protoMessageName: String = ModuleConfig.protoMessageName + ".AudioConfig" - public static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ - 1: .standard(proto: "codec2_enabled"), - 2: .standard(proto: "ptt_pin"), - 3: .same(proto: "bitrate"), - 4: .standard(proto: "i2s_ws"), - 5: .standard(proto: "i2s_sd"), - 6: .standard(proto: "i2s_din"), - 7: .standard(proto: "i2s_sck"), - ] + public static let _protobuf_nameMap = SwiftProtobuf._NameMap(bytecode: "\0\u{3}codec2_enabled\0\u{3}ptt_pin\0\u{1}bitrate\0\u{3}i2s_ws\0\u{3}i2s_sd\0\u{3}i2s_din\0\u{3}i2s_sck\0") public mutating func decodeMessage(decoder: inout D) throws { while let fieldNumber = try decoder.nextFieldNumber() { @@ -2057,27 +1887,12 @@ extension ModuleConfig.AudioConfig: SwiftProtobuf.Message, SwiftProtobuf._Messag } extension ModuleConfig.AudioConfig.Audio_Baud: SwiftProtobuf._ProtoNameProviding { - public static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ - 0: .same(proto: "CODEC2_DEFAULT"), - 1: .same(proto: "CODEC2_3200"), - 2: .same(proto: "CODEC2_2400"), - 3: .same(proto: "CODEC2_1600"), - 4: .same(proto: "CODEC2_1400"), - 5: .same(proto: "CODEC2_1300"), - 6: .same(proto: "CODEC2_1200"), - 7: .same(proto: "CODEC2_700"), - 8: .same(proto: "CODEC2_700B"), - ] + public static let _protobuf_nameMap = SwiftProtobuf._NameMap(bytecode: "\0\u{2}\0CODEC2_DEFAULT\0\u{1}CODEC2_3200\0\u{1}CODEC2_2400\0\u{1}CODEC2_1600\0\u{1}CODEC2_1400\0\u{1}CODEC2_1300\0\u{1}CODEC2_1200\0\u{1}CODEC2_700\0\u{1}CODEC2_700B\0") } extension ModuleConfig.PaxcounterConfig: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { public static let protoMessageName: String = ModuleConfig.protoMessageName + ".PaxcounterConfig" - public static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ - 1: .same(proto: "enabled"), - 2: .standard(proto: "paxcounter_update_interval"), - 3: .standard(proto: "wifi_threshold"), - 4: .standard(proto: "ble_threshold"), - ] + public static let _protobuf_nameMap = SwiftProtobuf._NameMap(bytecode: "\0\u{1}enabled\0\u{3}paxcounter_update_interval\0\u{3}wifi_threshold\0\u{3}ble_threshold\0") public mutating func decodeMessage(decoder: inout D) throws { while let fieldNumber = try decoder.nextFieldNumber() { @@ -2122,16 +1937,7 @@ extension ModuleConfig.PaxcounterConfig: SwiftProtobuf.Message, SwiftProtobuf._M extension ModuleConfig.SerialConfig: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { public static let protoMessageName: String = ModuleConfig.protoMessageName + ".SerialConfig" - public static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ - 1: .same(proto: "enabled"), - 2: .same(proto: "echo"), - 3: .same(proto: "rxd"), - 4: .same(proto: "txd"), - 5: .same(proto: "baud"), - 6: .same(proto: "timeout"), - 7: .same(proto: "mode"), - 8: .standard(proto: "override_console_serial_port"), - ] + public static let _protobuf_nameMap = SwiftProtobuf._NameMap(bytecode: "\0\u{1}enabled\0\u{1}echo\0\u{1}rxd\0\u{1}txd\0\u{1}baud\0\u{1}timeout\0\u{1}mode\0\u{3}override_console_serial_port\0") public mutating func decodeMessage(decoder: inout D) throws { while let fieldNumber = try decoder.nextFieldNumber() { @@ -2195,59 +2001,16 @@ extension ModuleConfig.SerialConfig: SwiftProtobuf.Message, SwiftProtobuf._Messa } extension ModuleConfig.SerialConfig.Serial_Baud: SwiftProtobuf._ProtoNameProviding { - public static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ - 0: .same(proto: "BAUD_DEFAULT"), - 1: .same(proto: "BAUD_110"), - 2: .same(proto: "BAUD_300"), - 3: .same(proto: "BAUD_600"), - 4: .same(proto: "BAUD_1200"), - 5: .same(proto: "BAUD_2400"), - 6: .same(proto: "BAUD_4800"), - 7: .same(proto: "BAUD_9600"), - 8: .same(proto: "BAUD_19200"), - 9: .same(proto: "BAUD_38400"), - 10: .same(proto: "BAUD_57600"), - 11: .same(proto: "BAUD_115200"), - 12: .same(proto: "BAUD_230400"), - 13: .same(proto: "BAUD_460800"), - 14: .same(proto: "BAUD_576000"), - 15: .same(proto: "BAUD_921600"), - ] + public static let _protobuf_nameMap = SwiftProtobuf._NameMap(bytecode: "\0\u{2}\0BAUD_DEFAULT\0\u{1}BAUD_110\0\u{1}BAUD_300\0\u{1}BAUD_600\0\u{1}BAUD_1200\0\u{1}BAUD_2400\0\u{1}BAUD_4800\0\u{1}BAUD_9600\0\u{1}BAUD_19200\0\u{1}BAUD_38400\0\u{1}BAUD_57600\0\u{1}BAUD_115200\0\u{1}BAUD_230400\0\u{1}BAUD_460800\0\u{1}BAUD_576000\0\u{1}BAUD_921600\0") } extension ModuleConfig.SerialConfig.Serial_Mode: SwiftProtobuf._ProtoNameProviding { - public static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ - 0: .same(proto: "DEFAULT"), - 1: .same(proto: "SIMPLE"), - 2: .same(proto: "PROTO"), - 3: .same(proto: "TEXTMSG"), - 4: .same(proto: "NMEA"), - 5: .same(proto: "CALTOPO"), - 6: .same(proto: "WS85"), - 7: .same(proto: "VE_DIRECT"), - 8: .same(proto: "MS_CONFIG"), - ] + 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") } extension ModuleConfig.ExternalNotificationConfig: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { public static let protoMessageName: String = ModuleConfig.protoMessageName + ".ExternalNotificationConfig" - public static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ - 1: .same(proto: "enabled"), - 2: .standard(proto: "output_ms"), - 3: .same(proto: "output"), - 8: .standard(proto: "output_vibra"), - 9: .standard(proto: "output_buzzer"), - 4: .same(proto: "active"), - 5: .standard(proto: "alert_message"), - 10: .standard(proto: "alert_message_vibra"), - 11: .standard(proto: "alert_message_buzzer"), - 6: .standard(proto: "alert_bell"), - 12: .standard(proto: "alert_bell_vibra"), - 13: .standard(proto: "alert_bell_buzzer"), - 7: .standard(proto: "use_pwm"), - 14: .standard(proto: "nag_timeout"), - 15: .standard(proto: "use_i2s_as_buzzer"), - ] + public static let _protobuf_nameMap = SwiftProtobuf._NameMap(bytecode: "\0\u{1}enabled\0\u{3}output_ms\0\u{1}output\0\u{1}active\0\u{3}alert_message\0\u{3}alert_bell\0\u{3}use_pwm\0\u{3}output_vibra\0\u{3}output_buzzer\0\u{3}alert_message_vibra\0\u{3}alert_message_buzzer\0\u{3}alert_bell_vibra\0\u{3}alert_bell_buzzer\0\u{3}nag_timeout\0\u{3}use_i2s_as_buzzer\0") public mutating func decodeMessage(decoder: inout D) throws { while let fieldNumber = try decoder.nextFieldNumber() { @@ -2347,14 +2110,7 @@ extension ModuleConfig.ExternalNotificationConfig: SwiftProtobuf.Message, SwiftP extension ModuleConfig.StoreForwardConfig: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { public static let protoMessageName: String = ModuleConfig.protoMessageName + ".StoreForwardConfig" - public static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ - 1: .same(proto: "enabled"), - 2: .same(proto: "heartbeat"), - 3: .same(proto: "records"), - 4: .standard(proto: "history_return_max"), - 5: .standard(proto: "history_return_window"), - 6: .standard(proto: "is_server"), - ] + public static let _protobuf_nameMap = SwiftProtobuf._NameMap(bytecode: "\0\u{1}enabled\0\u{1}heartbeat\0\u{1}records\0\u{3}history_return_max\0\u{3}history_return_window\0\u{3}is_server\0") public mutating func decodeMessage(decoder: inout D) throws { while let fieldNumber = try decoder.nextFieldNumber() { @@ -2409,12 +2165,7 @@ extension ModuleConfig.StoreForwardConfig: SwiftProtobuf.Message, SwiftProtobuf. extension ModuleConfig.RangeTestConfig: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { public static let protoMessageName: String = ModuleConfig.protoMessageName + ".RangeTestConfig" - public static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ - 1: .same(proto: "enabled"), - 2: .same(proto: "sender"), - 3: .same(proto: "save"), - 4: .standard(proto: "clear_on_reboot"), - ] + public static let _protobuf_nameMap = SwiftProtobuf._NameMap(bytecode: "\0\u{1}enabled\0\u{1}sender\0\u{1}save\0\u{3}clear_on_reboot\0") public mutating func decodeMessage(decoder: inout D) throws { while let fieldNumber = try decoder.nextFieldNumber() { @@ -2459,22 +2210,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 = [ - 1: .standard(proto: "device_update_interval"), - 2: .standard(proto: "environment_update_interval"), - 3: .standard(proto: "environment_measurement_enabled"), - 4: .standard(proto: "environment_screen_enabled"), - 5: .standard(proto: "environment_display_fahrenheit"), - 6: .standard(proto: "air_quality_enabled"), - 7: .standard(proto: "air_quality_interval"), - 8: .standard(proto: "power_measurement_enabled"), - 9: .standard(proto: "power_update_interval"), - 10: .standard(proto: "power_screen_enabled"), - 11: .standard(proto: "health_measurement_enabled"), - 12: .standard(proto: "health_update_interval"), - 13: .standard(proto: "health_screen_enabled"), - 14: .standard(proto: "device_telemetry_enabled"), - ] + 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 mutating func decodeMessage(decoder: inout D) throws { while let fieldNumber = try decoder.nextFieldNumber() { @@ -2569,19 +2305,7 @@ extension ModuleConfig.TelemetryConfig: SwiftProtobuf.Message, SwiftProtobuf._Me extension ModuleConfig.CannedMessageConfig: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { public static let protoMessageName: String = ModuleConfig.protoMessageName + ".CannedMessageConfig" - public static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ - 1: .standard(proto: "rotary1_enabled"), - 2: .standard(proto: "inputbroker_pin_a"), - 3: .standard(proto: "inputbroker_pin_b"), - 4: .standard(proto: "inputbroker_pin_press"), - 5: .standard(proto: "inputbroker_event_cw"), - 6: .standard(proto: "inputbroker_event_ccw"), - 7: .standard(proto: "inputbroker_event_press"), - 8: .standard(proto: "updown1_enabled"), - 9: .same(proto: "enabled"), - 10: .standard(proto: "allow_input_source"), - 11: .standard(proto: "send_bell"), - ] + public static let _protobuf_nameMap = SwiftProtobuf._NameMap(bytecode: "\0\u{3}rotary1_enabled\0\u{3}inputbroker_pin_a\0\u{3}inputbroker_pin_b\0\u{3}inputbroker_pin_press\0\u{3}inputbroker_event_cw\0\u{3}inputbroker_event_ccw\0\u{3}inputbroker_event_press\0\u{3}updown1_enabled\0\u{1}enabled\0\u{3}allow_input_source\0\u{3}send_bell\0") public mutating func decodeMessage(decoder: inout D) throws { while let fieldNumber = try decoder.nextFieldNumber() { @@ -2660,27 +2384,12 @@ extension ModuleConfig.CannedMessageConfig: SwiftProtobuf.Message, SwiftProtobuf } extension ModuleConfig.CannedMessageConfig.InputEventChar: SwiftProtobuf._ProtoNameProviding { - public static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ - 0: .same(proto: "NONE"), - 10: .same(proto: "SELECT"), - 17: .same(proto: "UP"), - 18: .same(proto: "DOWN"), - 19: .same(proto: "LEFT"), - 20: .same(proto: "RIGHT"), - 24: .same(proto: "CANCEL"), - 27: .same(proto: "BACK"), - ] + public static let _protobuf_nameMap = SwiftProtobuf._NameMap(bytecode: "\0\u{2}\0NONE\0\u{2}\u{a}SELECT\0\u{2}\u{7}UP\0\u{1}DOWN\0\u{1}LEFT\0\u{1}RIGHT\0\u{2}\u{4}CANCEL\0\u{2}\u{3}BACK\0") } extension ModuleConfig.AmbientLightingConfig: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { public static let protoMessageName: String = ModuleConfig.protoMessageName + ".AmbientLightingConfig" - public static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ - 1: .standard(proto: "led_state"), - 2: .same(proto: "current"), - 3: .same(proto: "red"), - 4: .same(proto: "green"), - 5: .same(proto: "blue"), - ] + public static let _protobuf_nameMap = SwiftProtobuf._NameMap(bytecode: "\0\u{3}led_state\0\u{1}current\0\u{1}red\0\u{1}green\0\u{1}blue\0") public mutating func decodeMessage(decoder: inout D) throws { while let fieldNumber = try decoder.nextFieldNumber() { @@ -2730,11 +2439,7 @@ extension ModuleConfig.AmbientLightingConfig: SwiftProtobuf.Message, SwiftProtob extension RemoteHardwarePin: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { public static let protoMessageName: String = _protobuf_package + ".RemoteHardwarePin" - public static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ - 1: .standard(proto: "gpio_pin"), - 2: .same(proto: "name"), - 3: .same(proto: "type"), - ] + public static let _protobuf_nameMap = SwiftProtobuf._NameMap(bytecode: "\0\u{3}gpio_pin\0\u{1}name\0\u{1}type\0") public mutating func decodeMessage(decoder: inout D) throws { while let fieldNumber = try decoder.nextFieldNumber() { diff --git a/MeshtasticProtobufs/Sources/meshtastic/mqtt.pb.swift b/MeshtasticProtobufs/Sources/meshtastic/mqtt.pb.swift index 94ed8cc9..4415e19a 100644 --- a/MeshtasticProtobufs/Sources/meshtastic/mqtt.pb.swift +++ b/MeshtasticProtobufs/Sources/meshtastic/mqtt.pb.swift @@ -1,5 +1,6 @@ // DO NOT EDIT. // swift-format-ignore-file +// swiftlint:disable all // // Generated by the Swift generator plugin for the protocol buffer compiler. // Source: meshtastic/mqtt.proto @@ -7,7 +8,6 @@ // For information on using the generated types, please see the documentation: // https://github.com/apple/swift-protobuf/ -import Foundation import SwiftProtobuf // If the compiler emits an error on this type, it is because this file @@ -22,7 +22,7 @@ fileprivate struct _GeneratedWithProtocGenSwiftVersion: SwiftProtobuf.ProtobufAP /// /// This message wraps a MeshPacket with extra metadata about the sender and how it arrived. -public struct ServiceEnvelope { +public struct ServiceEnvelope: 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. @@ -57,7 +57,7 @@ public struct ServiceEnvelope { /// /// Information about a node intended to be reported unencrypted to a map using MQTT. -public struct MapReport { +public struct MapReport: 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. @@ -126,22 +126,13 @@ public struct MapReport { public init() {} } -#if swift(>=5.5) && canImport(_Concurrency) -extension ServiceEnvelope: @unchecked Sendable {} -extension MapReport: @unchecked Sendable {} -#endif // swift(>=5.5) && canImport(_Concurrency) - // MARK: - Code below here is support for the SwiftProtobuf runtime. fileprivate let _protobuf_package = "meshtastic" extension ServiceEnvelope: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { public static let protoMessageName: String = _protobuf_package + ".ServiceEnvelope" - public static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ - 1: .same(proto: "packet"), - 2: .standard(proto: "channel_id"), - 3: .standard(proto: "gateway_id"), - ] + public static let _protobuf_nameMap = SwiftProtobuf._NameMap(bytecode: "\0\u{1}packet\0\u{3}channel_id\0\u{3}gateway_id\0") public mutating func decodeMessage(decoder: inout D) throws { while let fieldNumber = try decoder.nextFieldNumber() { @@ -185,22 +176,7 @@ extension ServiceEnvelope: SwiftProtobuf.Message, SwiftProtobuf._MessageImplemen extension MapReport: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { public static let protoMessageName: String = _protobuf_package + ".MapReport" - public static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ - 1: .standard(proto: "long_name"), - 2: .standard(proto: "short_name"), - 3: .same(proto: "role"), - 4: .standard(proto: "hw_model"), - 5: .standard(proto: "firmware_version"), - 6: .same(proto: "region"), - 7: .standard(proto: "modem_preset"), - 8: .standard(proto: "has_default_channel"), - 9: .standard(proto: "latitude_i"), - 10: .standard(proto: "longitude_i"), - 11: .same(proto: "altitude"), - 12: .standard(proto: "position_precision"), - 13: .standard(proto: "num_online_local_nodes"), - 14: .standard(proto: "has_opted_report_location"), - ] + public static let _protobuf_nameMap = SwiftProtobuf._NameMap(bytecode: "\0\u{3}long_name\0\u{3}short_name\0\u{1}role\0\u{3}hw_model\0\u{3}firmware_version\0\u{1}region\0\u{3}modem_preset\0\u{3}has_default_channel\0\u{3}latitude_i\0\u{3}longitude_i\0\u{1}altitude\0\u{3}position_precision\0\u{3}num_online_local_nodes\0\u{3}has_opted_report_location\0") public mutating func decodeMessage(decoder: inout D) throws { while let fieldNumber = try decoder.nextFieldNumber() { diff --git a/MeshtasticProtobufs/Sources/meshtastic/paxcount.pb.swift b/MeshtasticProtobufs/Sources/meshtastic/paxcount.pb.swift index cf8aa463..0864d08f 100644 --- a/MeshtasticProtobufs/Sources/meshtastic/paxcount.pb.swift +++ b/MeshtasticProtobufs/Sources/meshtastic/paxcount.pb.swift @@ -1,5 +1,6 @@ // DO NOT EDIT. // swift-format-ignore-file +// swiftlint:disable all // // Generated by the Swift generator plugin for the protocol buffer compiler. // Source: meshtastic/paxcount.proto @@ -7,7 +8,6 @@ // For information on using the generated types, please see the documentation: // https://github.com/apple/swift-protobuf/ -import Foundation import SwiftProtobuf // If the compiler emits an error on this type, it is because this file @@ -22,7 +22,7 @@ fileprivate struct _GeneratedWithProtocGenSwiftVersion: SwiftProtobuf.ProtobufAP /// /// TODO: REPLACE -public struct Paxcount { +public struct Paxcount: 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. @@ -44,21 +44,13 @@ public struct Paxcount { public init() {} } -#if swift(>=5.5) && canImport(_Concurrency) -extension Paxcount: @unchecked Sendable {} -#endif // swift(>=5.5) && canImport(_Concurrency) - // MARK: - Code below here is support for the SwiftProtobuf runtime. fileprivate let _protobuf_package = "meshtastic" extension Paxcount: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { public static let protoMessageName: String = _protobuf_package + ".Paxcount" - public static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ - 1: .same(proto: "wifi"), - 2: .same(proto: "ble"), - 3: .same(proto: "uptime"), - ] + public static let _protobuf_nameMap = SwiftProtobuf._NameMap(bytecode: "\0\u{1}wifi\0\u{1}ble\0\u{1}uptime\0") public mutating func decodeMessage(decoder: inout D) throws { while let fieldNumber = try decoder.nextFieldNumber() { diff --git a/MeshtasticProtobufs/Sources/meshtastic/portnums.pb.swift b/MeshtasticProtobufs/Sources/meshtastic/portnums.pb.swift index f9ff36f6..1d264b5b 100644 --- a/MeshtasticProtobufs/Sources/meshtastic/portnums.pb.swift +++ b/MeshtasticProtobufs/Sources/meshtastic/portnums.pb.swift @@ -1,5 +1,6 @@ // DO NOT EDIT. // swift-format-ignore-file +// swiftlint:disable all // // Generated by the Swift generator plugin for the protocol buffer compiler. // Source: meshtastic/portnums.proto @@ -7,7 +8,6 @@ // For information on using the generated types, please see the documentation: // https://github.com/apple/swift-protobuf/ -import Foundation import SwiftProtobuf // If the compiler emits an error on this type, it is because this file @@ -33,7 +33,7 @@ fileprivate struct _GeneratedWithProtocGenSwiftVersion: SwiftProtobuf.ProtobufAP /// Note: This was formerly a Type enum named 'typ' with the same id # /// We have change to this 'portnum' based scheme for specifying app handlers for particular payloads. /// This change is backwards compatible by treating the legacy OPAQUE/CLEAR_TEXT values identically. -public enum PortNum: SwiftProtobuf.Enum { +public enum PortNum: SwiftProtobuf.Enum, Swift.CaseIterable { public typealias RawValue = Int /// @@ -131,6 +131,13 @@ public enum PortNum: SwiftProtobuf.Enum { /// ENCODING: protobuf case paxcounterApp // = 34 + /// + /// Store and Forward++ module included in the firmware + /// ENCODING: protobuf + /// This module is specifically for Native Linux nodes, and provides a Git-style + /// chain of messages. + case storeForwardPlusplusApp // = 35 + /// /// Provides a hardware serial interface to send and receive from the Meshtastic network. /// Connect to the RX/TX pins of a device with 38400 8N1. Packets received from the Meshtastic @@ -246,6 +253,7 @@ public enum PortNum: SwiftProtobuf.Enum { case 32: self = .replyApp case 33: self = .ipTunnelApp case 34: self = .paxcounterApp + case 35: self = .storeForwardPlusplusApp case 64: self = .serialApp case 65: self = .storeForwardApp case 66: self = .rangeTestApp @@ -284,6 +292,7 @@ public enum PortNum: SwiftProtobuf.Enum { case .replyApp: return 32 case .ipTunnelApp: return 33 case .paxcounterApp: return 34 + case .storeForwardPlusplusApp: return 35 case .serialApp: return 64 case .storeForwardApp: return 65 case .rangeTestApp: return 66 @@ -304,11 +313,6 @@ public enum PortNum: SwiftProtobuf.Enum { } } -} - -#if swift(>=4.2) - -extension PortNum: CaseIterable { // The compiler won't synthesize support with the UNRECOGNIZED case. public static let allCases: [PortNum] = [ .unknownApp, @@ -327,6 +331,7 @@ extension PortNum: CaseIterable { .replyApp, .ipTunnelApp, .paxcounterApp, + .storeForwardPlusplusApp, .serialApp, .storeForwardApp, .rangeTestApp, @@ -344,49 +349,11 @@ extension PortNum: CaseIterable { .atakForwarder, .max, ] + } -#endif // swift(>=4.2) - -#if swift(>=5.5) && canImport(_Concurrency) -extension PortNum: @unchecked Sendable {} -#endif // swift(>=5.5) && canImport(_Concurrency) - // MARK: - Code below here is support for the SwiftProtobuf runtime. extension PortNum: SwiftProtobuf._ProtoNameProviding { - public static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ - 0: .same(proto: "UNKNOWN_APP"), - 1: .same(proto: "TEXT_MESSAGE_APP"), - 2: .same(proto: "REMOTE_HARDWARE_APP"), - 3: .same(proto: "POSITION_APP"), - 4: .same(proto: "NODEINFO_APP"), - 5: .same(proto: "ROUTING_APP"), - 6: .same(proto: "ADMIN_APP"), - 7: .same(proto: "TEXT_MESSAGE_COMPRESSED_APP"), - 8: .same(proto: "WAYPOINT_APP"), - 9: .same(proto: "AUDIO_APP"), - 10: .same(proto: "DETECTION_SENSOR_APP"), - 11: .same(proto: "ALERT_APP"), - 12: .same(proto: "KEY_VERIFICATION_APP"), - 32: .same(proto: "REPLY_APP"), - 33: .same(proto: "IP_TUNNEL_APP"), - 34: .same(proto: "PAXCOUNTER_APP"), - 64: .same(proto: "SERIAL_APP"), - 65: .same(proto: "STORE_FORWARD_APP"), - 66: .same(proto: "RANGE_TEST_APP"), - 67: .same(proto: "TELEMETRY_APP"), - 68: .same(proto: "ZPS_APP"), - 69: .same(proto: "SIMULATOR_APP"), - 70: .same(proto: "TRACEROUTE_APP"), - 71: .same(proto: "NEIGHBORINFO_APP"), - 72: .same(proto: "ATAK_PLUGIN"), - 73: .same(proto: "MAP_REPORT_APP"), - 74: .same(proto: "POWERSTRESS_APP"), - 76: .same(proto: "RETICULUM_TUNNEL_APP"), - 77: .same(proto: "CAYENNE_APP"), - 256: .same(proto: "PRIVATE_APP"), - 257: .same(proto: "ATAK_FORWARDER"), - 511: .same(proto: "MAX"), - ] + public static let _protobuf_nameMap = SwiftProtobuf._NameMap(bytecode: "\0\u{2}\0UNKNOWN_APP\0\u{1}TEXT_MESSAGE_APP\0\u{1}REMOTE_HARDWARE_APP\0\u{1}POSITION_APP\0\u{1}NODEINFO_APP\0\u{1}ROUTING_APP\0\u{1}ADMIN_APP\0\u{1}TEXT_MESSAGE_COMPRESSED_APP\0\u{1}WAYPOINT_APP\0\u{1}AUDIO_APP\0\u{1}DETECTION_SENSOR_APP\0\u{1}ALERT_APP\0\u{1}KEY_VERIFICATION_APP\0\u{2}\u{14}REPLY_APP\0\u{1}IP_TUNNEL_APP\0\u{1}PAXCOUNTER_APP\0\u{1}STORE_FORWARD_PLUSPLUS_APP\0\u{2}\u{1d}SERIAL_APP\0\u{1}STORE_FORWARD_APP\0\u{1}RANGE_TEST_APP\0\u{1}TELEMETRY_APP\0\u{1}ZPS_APP\0\u{1}SIMULATOR_APP\0\u{1}TRACEROUTE_APP\0\u{1}NEIGHBORINFO_APP\0\u{1}ATAK_PLUGIN\0\u{1}MAP_REPORT_APP\0\u{1}POWERSTRESS_APP\0\u{2}\u{2}RETICULUM_TUNNEL_APP\0\u{1}CAYENNE_APP\0\u{2}s\u{2}PRIVATE_APP\0\u{1}ATAK_FORWARDER\0\u{2}~\u{3}MAX\0") } diff --git a/MeshtasticProtobufs/Sources/meshtastic/powermon.pb.swift b/MeshtasticProtobufs/Sources/meshtastic/powermon.pb.swift index 92f3c5ce..7332ba7c 100644 --- a/MeshtasticProtobufs/Sources/meshtastic/powermon.pb.swift +++ b/MeshtasticProtobufs/Sources/meshtastic/powermon.pb.swift @@ -1,5 +1,6 @@ // DO NOT EDIT. // swift-format-ignore-file +// swiftlint:disable all // // Generated by the Swift generator plugin for the protocol buffer compiler. // Source: meshtastic/powermon.proto @@ -7,7 +8,6 @@ // For information on using the generated types, please see the documentation: // https://github.com/apple/swift-protobuf/ -import Foundation import SwiftProtobuf // If the compiler emits an error on this type, it is because this file @@ -22,7 +22,7 @@ fileprivate struct _GeneratedWithProtocGenSwiftVersion: SwiftProtobuf.ProtobufAP /// Note: There are no 'PowerMon' messages normally in use (PowerMons are sent only as structured logs - slogs). /// But we wrap our State enum in this message to effectively nest a namespace (without our linter yelling at us) -public struct PowerMon { +public struct PowerMon: 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. @@ -31,7 +31,7 @@ public struct PowerMon { /// Any significant power changing event in meshtastic should be tagged with a powermon state transition. /// If you are making new meshtastic features feel free to add new entries at the end of this definition. - public enum State: SwiftProtobuf.Enum { + public enum State: SwiftProtobuf.Enum, Swift.CaseIterable { public typealias RawValue = Int case none // = 0 case cpuDeepSleep // = 1 @@ -104,37 +104,31 @@ public struct PowerMon { } } + // The compiler won't synthesize support with the UNRECOGNIZED case. + public static let allCases: [PowerMon.State] = [ + .none, + .cpuDeepSleep, + .cpuLightSleep, + .vext1On, + .loraRxon, + .loraTxon, + .loraRxactive, + .btOn, + .ledOn, + .screenOn, + .screenDrawing, + .wifiOn, + .gpsActive, + ] + } public init() {} } -#if swift(>=4.2) - -extension PowerMon.State: CaseIterable { - // The compiler won't synthesize support with the UNRECOGNIZED case. - public static let allCases: [PowerMon.State] = [ - .none, - .cpuDeepSleep, - .cpuLightSleep, - .vext1On, - .loraRxon, - .loraTxon, - .loraRxactive, - .btOn, - .ledOn, - .screenOn, - .screenDrawing, - .wifiOn, - .gpsActive, - ] -} - -#endif // swift(>=4.2) - /// /// PowerStress testing support via the C++ PowerStress module -public struct PowerStressMessage { +public struct PowerStressMessage: 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. @@ -151,7 +145,7 @@ public struct PowerStressMessage { /// What operation would we like the UUT to perform. /// note: senders should probably set want_response in their request packets, so that they can know when the state /// machine has started processing their request - public enum Opcode: SwiftProtobuf.Enum { + public enum Opcode: SwiftProtobuf.Enum, Swift.CaseIterable { public typealias RawValue = Int /// @@ -272,48 +266,35 @@ public struct PowerStressMessage { } } + // The compiler won't synthesize support with the UNRECOGNIZED case. + public static let allCases: [PowerStressMessage.Opcode] = [ + .unset, + .printInfo, + .forceQuiet, + .endQuiet, + .screenOn, + .screenOff, + .cpuIdle, + .cpuDeepsleep, + .cpuFullon, + .ledOn, + .ledOff, + .loraOff, + .loraTx, + .loraRx, + .btOff, + .btOn, + .wifiOff, + .wifiOn, + .gpsOff, + .gpsOn, + ] + } public init() {} } -#if swift(>=4.2) - -extension PowerStressMessage.Opcode: CaseIterable { - // The compiler won't synthesize support with the UNRECOGNIZED case. - public static let allCases: [PowerStressMessage.Opcode] = [ - .unset, - .printInfo, - .forceQuiet, - .endQuiet, - .screenOn, - .screenOff, - .cpuIdle, - .cpuDeepsleep, - .cpuFullon, - .ledOn, - .ledOff, - .loraOff, - .loraTx, - .loraRx, - .btOff, - .btOn, - .wifiOff, - .wifiOn, - .gpsOff, - .gpsOn, - ] -} - -#endif // swift(>=4.2) - -#if swift(>=5.5) && canImport(_Concurrency) -extension PowerMon: @unchecked Sendable {} -extension PowerMon.State: @unchecked Sendable {} -extension PowerStressMessage: @unchecked Sendable {} -extension PowerStressMessage.Opcode: @unchecked Sendable {} -#endif // swift(>=5.5) && canImport(_Concurrency) - // MARK: - Code below here is support for the SwiftProtobuf runtime. fileprivate let _protobuf_package = "meshtastic" @@ -323,8 +304,8 @@ extension PowerMon: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationB public static let _protobuf_nameMap = SwiftProtobuf._NameMap() public mutating func decodeMessage(decoder: inout D) throws { - while let _ = try decoder.nextFieldNumber() { - } + // Load everything into unknown fields + while try decoder.nextFieldNumber() != nil {} } public func traverse(visitor: inout V) throws { @@ -338,29 +319,12 @@ extension PowerMon: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationB } extension PowerMon.State: SwiftProtobuf._ProtoNameProviding { - public static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ - 0: .same(proto: "None"), - 1: .same(proto: "CPU_DeepSleep"), - 2: .same(proto: "CPU_LightSleep"), - 4: .same(proto: "Vext1_On"), - 8: .same(proto: "Lora_RXOn"), - 16: .same(proto: "Lora_TXOn"), - 32: .same(proto: "Lora_RXActive"), - 64: .same(proto: "BT_On"), - 128: .same(proto: "LED_On"), - 256: .same(proto: "Screen_On"), - 512: .same(proto: "Screen_Drawing"), - 1024: .same(proto: "Wifi_On"), - 2048: .same(proto: "GPS_Active"), - ] + public static let _protobuf_nameMap = SwiftProtobuf._NameMap(bytecode: "\0\u{2}\0None\0\u{1}CPU_DeepSleep\0\u{1}CPU_LightSleep\0\u{2}\u{2}Vext1_On\0\u{2}\u{4}Lora_RXOn\0\u{2}\u{8}Lora_TXOn\0\u{2}\u{10}Lora_RXActive\0\u{2} BT_On\0\u{2}@\u{1}LED_On\0\u{2}@\u{2}Screen_On\0\u{2}@\u{4}Screen_Drawing\0\u{2}@\u{8}Wifi_On\0\u{2}@\u{10}GPS_Active\0") } extension PowerStressMessage: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { public static let protoMessageName: String = _protobuf_package + ".PowerStressMessage" - public static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ - 1: .same(proto: "cmd"), - 2: .standard(proto: "num_seconds"), - ] + public static let _protobuf_nameMap = SwiftProtobuf._NameMap(bytecode: "\0\u{1}cmd\0\u{3}num_seconds\0") public mutating func decodeMessage(decoder: inout D) throws { while let fieldNumber = try decoder.nextFieldNumber() { @@ -379,7 +343,7 @@ extension PowerStressMessage: SwiftProtobuf.Message, SwiftProtobuf._MessageImple if self.cmd != .unset { try visitor.visitSingularEnumField(value: self.cmd, fieldNumber: 1) } - if self.numSeconds != 0 { + if self.numSeconds.bitPattern != 0 { try visitor.visitSingularFloatField(value: self.numSeconds, fieldNumber: 2) } try unknownFields.traverse(visitor: &visitor) @@ -394,26 +358,5 @@ extension PowerStressMessage: SwiftProtobuf.Message, SwiftProtobuf._MessageImple } extension PowerStressMessage.Opcode: SwiftProtobuf._ProtoNameProviding { - public static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ - 0: .same(proto: "UNSET"), - 1: .same(proto: "PRINT_INFO"), - 2: .same(proto: "FORCE_QUIET"), - 3: .same(proto: "END_QUIET"), - 16: .same(proto: "SCREEN_ON"), - 17: .same(proto: "SCREEN_OFF"), - 32: .same(proto: "CPU_IDLE"), - 33: .same(proto: "CPU_DEEPSLEEP"), - 34: .same(proto: "CPU_FULLON"), - 48: .same(proto: "LED_ON"), - 49: .same(proto: "LED_OFF"), - 64: .same(proto: "LORA_OFF"), - 65: .same(proto: "LORA_TX"), - 66: .same(proto: "LORA_RX"), - 80: .same(proto: "BT_OFF"), - 81: .same(proto: "BT_ON"), - 96: .same(proto: "WIFI_OFF"), - 97: .same(proto: "WIFI_ON"), - 112: .same(proto: "GPS_OFF"), - 113: .same(proto: "GPS_ON"), - ] + public static let _protobuf_nameMap = SwiftProtobuf._NameMap(bytecode: "\0\u{2}\0UNSET\0\u{1}PRINT_INFO\0\u{1}FORCE_QUIET\0\u{1}END_QUIET\0\u{2}\u{d}SCREEN_ON\0\u{1}SCREEN_OFF\0\u{2}\u{f}CPU_IDLE\0\u{1}CPU_DEEPSLEEP\0\u{1}CPU_FULLON\0\u{2}\u{e}LED_ON\0\u{1}LED_OFF\0\u{2}\u{f}LORA_OFF\0\u{1}LORA_TX\0\u{1}LORA_RX\0\u{2}\u{e}BT_OFF\0\u{1}BT_ON\0\u{2}\u{f}WIFI_OFF\0\u{1}WIFI_ON\0\u{2}\u{f}GPS_OFF\0\u{1}GPS_ON\0") } diff --git a/MeshtasticProtobufs/Sources/meshtastic/remote_hardware.pb.swift b/MeshtasticProtobufs/Sources/meshtastic/remote_hardware.pb.swift index ac6eeb26..001db067 100644 --- a/MeshtasticProtobufs/Sources/meshtastic/remote_hardware.pb.swift +++ b/MeshtasticProtobufs/Sources/meshtastic/remote_hardware.pb.swift @@ -1,5 +1,6 @@ // DO NOT EDIT. // swift-format-ignore-file +// swiftlint:disable all // // Generated by the Swift generator plugin for the protocol buffer compiler. // Source: meshtastic/remote_hardware.proto @@ -7,7 +8,6 @@ // For information on using the generated types, please see the documentation: // https://github.com/apple/swift-protobuf/ -import Foundation import SwiftProtobuf // If the compiler emits an error on this type, it is because this file @@ -30,7 +30,7 @@ fileprivate struct _GeneratedWithProtocGenSwiftVersion: SwiftProtobuf.ProtobufAP /// because no security yet (beyond the channel mechanism). /// It should be off by default and then protected based on some TBD mechanism /// (a special channel once multichannel support is included?) -public struct HardwareMessage { +public struct HardwareMessage: 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. @@ -52,7 +52,7 @@ public struct HardwareMessage { /// /// TODO: REPLACE - public enum TypeEnum: SwiftProtobuf.Enum { + public enum TypeEnum: SwiftProtobuf.Enum, Swift.CaseIterable { public typealias RawValue = Int /// @@ -110,43 +110,28 @@ public struct HardwareMessage { } } + // The compiler won't synthesize support with the UNRECOGNIZED case. + public static let allCases: [HardwareMessage.TypeEnum] = [ + .unset, + .writeGpios, + .watchGpios, + .gpiosChanged, + .readGpios, + .readGpiosReply, + ] + } public init() {} } -#if swift(>=4.2) - -extension HardwareMessage.TypeEnum: CaseIterable { - // The compiler won't synthesize support with the UNRECOGNIZED case. - public static let allCases: [HardwareMessage.TypeEnum] = [ - .unset, - .writeGpios, - .watchGpios, - .gpiosChanged, - .readGpios, - .readGpiosReply, - ] -} - -#endif // swift(>=4.2) - -#if swift(>=5.5) && canImport(_Concurrency) -extension HardwareMessage: @unchecked Sendable {} -extension HardwareMessage.TypeEnum: @unchecked Sendable {} -#endif // swift(>=5.5) && canImport(_Concurrency) - // MARK: - Code below here is support for the SwiftProtobuf runtime. fileprivate let _protobuf_package = "meshtastic" extension HardwareMessage: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { public static let protoMessageName: String = _protobuf_package + ".HardwareMessage" - public static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ - 1: .same(proto: "type"), - 2: .standard(proto: "gpio_mask"), - 3: .standard(proto: "gpio_value"), - ] + public static let _protobuf_nameMap = SwiftProtobuf._NameMap(bytecode: "\0\u{1}type\0\u{3}gpio_mask\0\u{3}gpio_value\0") public mutating func decodeMessage(decoder: inout D) throws { while let fieldNumber = try decoder.nextFieldNumber() { @@ -185,12 +170,5 @@ extension HardwareMessage: SwiftProtobuf.Message, SwiftProtobuf._MessageImplemen } extension HardwareMessage.TypeEnum: SwiftProtobuf._ProtoNameProviding { - public static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ - 0: .same(proto: "UNSET"), - 1: .same(proto: "WRITE_GPIOS"), - 2: .same(proto: "WATCH_GPIOS"), - 3: .same(proto: "GPIOS_CHANGED"), - 4: .same(proto: "READ_GPIOS"), - 5: .same(proto: "READ_GPIOS_REPLY"), - ] + public static let _protobuf_nameMap = SwiftProtobuf._NameMap(bytecode: "\0\u{2}\0UNSET\0\u{1}WRITE_GPIOS\0\u{1}WATCH_GPIOS\0\u{1}GPIOS_CHANGED\0\u{1}READ_GPIOS\0\u{1}READ_GPIOS_REPLY\0") } diff --git a/MeshtasticProtobufs/Sources/meshtastic/rtttl.pb.swift b/MeshtasticProtobufs/Sources/meshtastic/rtttl.pb.swift index 6fdf3208..0c114acb 100644 --- a/MeshtasticProtobufs/Sources/meshtastic/rtttl.pb.swift +++ b/MeshtasticProtobufs/Sources/meshtastic/rtttl.pb.swift @@ -1,5 +1,6 @@ // DO NOT EDIT. // swift-format-ignore-file +// swiftlint:disable all // // Generated by the Swift generator plugin for the protocol buffer compiler. // Source: meshtastic/rtttl.proto @@ -7,7 +8,6 @@ // For information on using the generated types, please see the documentation: // https://github.com/apple/swift-protobuf/ -import Foundation import SwiftProtobuf // If the compiler emits an error on this type, it is because this file @@ -22,7 +22,7 @@ fileprivate struct _GeneratedWithProtocGenSwiftVersion: SwiftProtobuf.ProtobufAP /// /// Canned message module configuration. -public struct RTTTLConfig { +public struct RTTTLConfig: 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. @@ -36,19 +36,13 @@ public struct RTTTLConfig { public init() {} } -#if swift(>=5.5) && canImport(_Concurrency) -extension RTTTLConfig: @unchecked Sendable {} -#endif // swift(>=5.5) && canImport(_Concurrency) - // MARK: - Code below here is support for the SwiftProtobuf runtime. fileprivate let _protobuf_package = "meshtastic" extension RTTTLConfig: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { public static let protoMessageName: String = _protobuf_package + ".RTTTLConfig" - public static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ - 1: .same(proto: "ringtone"), - ] + public static let _protobuf_nameMap = SwiftProtobuf._NameMap(bytecode: "\0\u{1}ringtone\0") public mutating func decodeMessage(decoder: inout D) throws { while let fieldNumber = try decoder.nextFieldNumber() { diff --git a/MeshtasticProtobufs/Sources/meshtastic/storeforward.pb.swift b/MeshtasticProtobufs/Sources/meshtastic/storeforward.pb.swift index 54efa77b..a0534387 100644 --- a/MeshtasticProtobufs/Sources/meshtastic/storeforward.pb.swift +++ b/MeshtasticProtobufs/Sources/meshtastic/storeforward.pb.swift @@ -1,5 +1,6 @@ // DO NOT EDIT. // swift-format-ignore-file +// swiftlint:disable all // // Generated by the Swift generator plugin for the protocol buffer compiler. // Source: meshtastic/storeforward.proto @@ -22,7 +23,7 @@ fileprivate struct _GeneratedWithProtocGenSwiftVersion: SwiftProtobuf.ProtobufAP /// /// TODO: REPLACE -public struct StoreAndForward { +public struct StoreAndForward: 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. @@ -79,7 +80,7 @@ public struct StoreAndForward { /// /// TODO: REPLACE - public enum OneOf_Variant: Equatable { + public enum OneOf_Variant: Equatable, Sendable { /// /// TODO: REPLACE case stats(StoreAndForward.Statistics) @@ -93,38 +94,12 @@ public struct StoreAndForward { /// Text from history message. case text(Data) - #if !swift(>=4.1) - public static func ==(lhs: StoreAndForward.OneOf_Variant, rhs: StoreAndForward.OneOf_Variant) -> Bool { - // 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 (lhs, rhs) { - case (.stats, .stats): return { - guard case .stats(let l) = lhs, case .stats(let r) = rhs else { preconditionFailure() } - return l == r - }() - case (.history, .history): return { - guard case .history(let l) = lhs, case .history(let r) = rhs else { preconditionFailure() } - return l == r - }() - case (.heartbeat, .heartbeat): return { - guard case .heartbeat(let l) = lhs, case .heartbeat(let r) = rhs else { preconditionFailure() } - return l == r - }() - case (.text, .text): return { - guard case .text(let l) = lhs, case .text(let r) = rhs else { preconditionFailure() } - return l == r - }() - default: return false - } - } - #endif } /// /// 001 - 063 = From Router /// 064 - 127 = From Client - public enum RequestResponse: SwiftProtobuf.Enum { + public enum RequestResponse: SwiftProtobuf.Enum, Swift.CaseIterable { public typealias RawValue = Int /// @@ -242,11 +217,31 @@ public struct StoreAndForward { } } + // The compiler won't synthesize support with the UNRECOGNIZED case. + public static let allCases: [StoreAndForward.RequestResponse] = [ + .unset, + .routerError, + .routerHeartbeat, + .routerPing, + .routerPong, + .routerBusy, + .routerHistory, + .routerStats, + .routerTextDirect, + .routerTextBroadcast, + .clientError, + .clientHistory, + .clientStats, + .clientPing, + .clientPong, + .clientAbort, + ] + } /// /// TODO: REPLACE - public struct Statistics { + public struct Statistics: 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. @@ -294,7 +289,7 @@ public struct StoreAndForward { /// /// TODO: REPLACE - public struct History { + public struct History: 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. @@ -319,7 +314,7 @@ public struct StoreAndForward { /// /// TODO: REPLACE - public struct Heartbeat { + public struct Heartbeat: 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. @@ -340,54 +335,13 @@ public struct StoreAndForward { public init() {} } -#if swift(>=4.2) - -extension StoreAndForward.RequestResponse: CaseIterable { - // The compiler won't synthesize support with the UNRECOGNIZED case. - public static let allCases: [StoreAndForward.RequestResponse] = [ - .unset, - .routerError, - .routerHeartbeat, - .routerPing, - .routerPong, - .routerBusy, - .routerHistory, - .routerStats, - .routerTextDirect, - .routerTextBroadcast, - .clientError, - .clientHistory, - .clientStats, - .clientPing, - .clientPong, - .clientAbort, - ] -} - -#endif // swift(>=4.2) - -#if swift(>=5.5) && canImport(_Concurrency) -extension StoreAndForward: @unchecked Sendable {} -extension StoreAndForward.OneOf_Variant: @unchecked Sendable {} -extension StoreAndForward.RequestResponse: @unchecked Sendable {} -extension StoreAndForward.Statistics: @unchecked Sendable {} -extension StoreAndForward.History: @unchecked Sendable {} -extension StoreAndForward.Heartbeat: @unchecked Sendable {} -#endif // swift(>=5.5) && canImport(_Concurrency) - // MARK: - Code below here is support for the SwiftProtobuf runtime. fileprivate let _protobuf_package = "meshtastic" extension StoreAndForward: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { public static let protoMessageName: String = _protobuf_package + ".StoreAndForward" - public static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ - 1: .same(proto: "rr"), - 2: .same(proto: "stats"), - 3: .same(proto: "history"), - 4: .same(proto: "heartbeat"), - 5: .same(proto: "text"), - ] + public static let _protobuf_nameMap = SwiftProtobuf._NameMap(bytecode: "\0\u{1}rr\0\u{1}stats\0\u{1}history\0\u{1}heartbeat\0\u{1}text\0") public mutating func decodeMessage(decoder: inout D) throws { while let fieldNumber = try decoder.nextFieldNumber() { @@ -487,39 +441,12 @@ extension StoreAndForward: SwiftProtobuf.Message, SwiftProtobuf._MessageImplemen } extension StoreAndForward.RequestResponse: SwiftProtobuf._ProtoNameProviding { - public static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ - 0: .same(proto: "UNSET"), - 1: .same(proto: "ROUTER_ERROR"), - 2: .same(proto: "ROUTER_HEARTBEAT"), - 3: .same(proto: "ROUTER_PING"), - 4: .same(proto: "ROUTER_PONG"), - 5: .same(proto: "ROUTER_BUSY"), - 6: .same(proto: "ROUTER_HISTORY"), - 7: .same(proto: "ROUTER_STATS"), - 8: .same(proto: "ROUTER_TEXT_DIRECT"), - 9: .same(proto: "ROUTER_TEXT_BROADCAST"), - 64: .same(proto: "CLIENT_ERROR"), - 65: .same(proto: "CLIENT_HISTORY"), - 66: .same(proto: "CLIENT_STATS"), - 67: .same(proto: "CLIENT_PING"), - 68: .same(proto: "CLIENT_PONG"), - 106: .same(proto: "CLIENT_ABORT"), - ] + public static let _protobuf_nameMap = SwiftProtobuf._NameMap(bytecode: "\0\u{2}\0UNSET\0\u{1}ROUTER_ERROR\0\u{1}ROUTER_HEARTBEAT\0\u{1}ROUTER_PING\0\u{1}ROUTER_PONG\0\u{1}ROUTER_BUSY\0\u{1}ROUTER_HISTORY\0\u{1}ROUTER_STATS\0\u{1}ROUTER_TEXT_DIRECT\0\u{1}ROUTER_TEXT_BROADCAST\0\u{2}7CLIENT_ERROR\0\u{1}CLIENT_HISTORY\0\u{1}CLIENT_STATS\0\u{1}CLIENT_PING\0\u{1}CLIENT_PONG\0\u{2}&CLIENT_ABORT\0") } extension StoreAndForward.Statistics: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { public static let protoMessageName: String = StoreAndForward.protoMessageName + ".Statistics" - public static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ - 1: .standard(proto: "messages_total"), - 2: .standard(proto: "messages_saved"), - 3: .standard(proto: "messages_max"), - 4: .standard(proto: "up_time"), - 5: .same(proto: "requests"), - 6: .standard(proto: "requests_history"), - 7: .same(proto: "heartbeat"), - 8: .standard(proto: "return_max"), - 9: .standard(proto: "return_window"), - ] + public static let _protobuf_nameMap = SwiftProtobuf._NameMap(bytecode: "\0\u{3}messages_total\0\u{3}messages_saved\0\u{3}messages_max\0\u{3}up_time\0\u{1}requests\0\u{3}requests_history\0\u{1}heartbeat\0\u{3}return_max\0\u{3}return_window\0") public mutating func decodeMessage(decoder: inout D) throws { while let fieldNumber = try decoder.nextFieldNumber() { @@ -589,11 +516,7 @@ extension StoreAndForward.Statistics: SwiftProtobuf.Message, SwiftProtobuf._Mess extension StoreAndForward.History: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { public static let protoMessageName: String = StoreAndForward.protoMessageName + ".History" - public static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ - 1: .standard(proto: "history_messages"), - 2: .same(proto: "window"), - 3: .standard(proto: "last_request"), - ] + public static let _protobuf_nameMap = SwiftProtobuf._NameMap(bytecode: "\0\u{3}history_messages\0\u{1}window\0\u{3}last_request\0") public mutating func decodeMessage(decoder: inout D) throws { while let fieldNumber = try decoder.nextFieldNumber() { @@ -633,10 +556,7 @@ extension StoreAndForward.History: SwiftProtobuf.Message, SwiftProtobuf._Message extension StoreAndForward.Heartbeat: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { public static let protoMessageName: String = StoreAndForward.protoMessageName + ".Heartbeat" - public static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ - 1: .same(proto: "period"), - 2: .same(proto: "secondary"), - ] + public static let _protobuf_nameMap = SwiftProtobuf._NameMap(bytecode: "\0\u{1}period\0\u{1}secondary\0") public mutating func decodeMessage(decoder: inout D) throws { while let fieldNumber = try decoder.nextFieldNumber() { diff --git a/MeshtasticProtobufs/Sources/meshtastic/telemetry.pb.swift b/MeshtasticProtobufs/Sources/meshtastic/telemetry.pb.swift index 1173189c..5ad5fad3 100644 --- a/MeshtasticProtobufs/Sources/meshtastic/telemetry.pb.swift +++ b/MeshtasticProtobufs/Sources/meshtastic/telemetry.pb.swift @@ -1,5 +1,6 @@ // DO NOT EDIT. // swift-format-ignore-file +// swiftlint:disable all // // Generated by the Swift generator plugin for the protocol buffer compiler. // Source: meshtastic/telemetry.proto @@ -7,7 +8,6 @@ // For information on using the generated types, please see the documentation: // https://github.com/apple/swift-protobuf/ -import Foundation import SwiftProtobuf // If the compiler emits an error on this type, it is because this file @@ -22,7 +22,7 @@ fileprivate struct _GeneratedWithProtocGenSwiftVersion: SwiftProtobuf.ProtobufAP /// /// Supported I2C Sensors for telemetry in Meshtastic -public enum TelemetrySensorType: SwiftProtobuf.Enum { +public enum TelemetrySensorType: SwiftProtobuf.Enum, Swift.CaseIterable { public typealias RawValue = Int /// @@ -204,6 +204,10 @@ public enum TelemetrySensorType: SwiftProtobuf.Enum { /// /// TSL2561 light sensor case tsl2561 // = 44 + + /// + /// BH1750 light sensor + case bh1750 // = 45 case UNRECOGNIZED(Int) public init() { @@ -257,6 +261,7 @@ public enum TelemetrySensorType: SwiftProtobuf.Enum { case 42: self = .sfa30 case 43: self = .sen5X case 44: self = .tsl2561 + case 45: self = .bh1750 default: self = .UNRECOGNIZED(rawValue) } } @@ -308,15 +313,11 @@ public enum TelemetrySensorType: SwiftProtobuf.Enum { case .sfa30: return 42 case .sen5X: return 43 case .tsl2561: return 44 + case .bh1750: return 45 case .UNRECOGNIZED(let i): return i } } -} - -#if swift(>=4.2) - -extension TelemetrySensorType: CaseIterable { // The compiler won't synthesize support with the UNRECOGNIZED case. public static let allCases: [TelemetrySensorType] = [ .sensorUnset, @@ -364,14 +365,14 @@ extension TelemetrySensorType: CaseIterable { .sfa30, .sen5X, .tsl2561, + .bh1750, ] -} -#endif // swift(>=4.2) +} /// /// Key native device metrics such as battery level -public struct DeviceMetrics { +public struct DeviceMetrics: 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. @@ -444,7 +445,7 @@ public struct DeviceMetrics { /// /// Weather station or other environmental metrics -public struct EnvironmentMetrics { +public struct EnvironmentMetrics: @unchecked 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. @@ -702,7 +703,7 @@ public struct EnvironmentMetrics { /// /// Power Metrics (voltage / current / etc) -public struct PowerMetrics { +public struct PowerMetrics: 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. @@ -907,7 +908,7 @@ public struct PowerMetrics { /// /// Air quality metrics -public struct AirQualityMetrics { +public struct AirQualityMetrics: @unchecked 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. @@ -1196,7 +1197,7 @@ public struct AirQualityMetrics { /// /// Local device mesh statistics -public struct LocalStats { +public struct LocalStats: 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. @@ -1255,6 +1256,10 @@ public struct LocalStats { /// Number of bytes free in the heap public var heapFreeBytes: UInt32 = 0 + /// + /// Number of packets that were dropped because the transmit queue was full. + public var numTxDropped: UInt32 = 0 + public var unknownFields = SwiftProtobuf.UnknownStorage() public init() {} @@ -1262,7 +1267,7 @@ public struct LocalStats { /// /// Health telemetry metrics -public struct HealthMetrics { +public struct HealthMetrics: 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. @@ -1311,7 +1316,7 @@ public struct HealthMetrics { /// /// Linux host metrics -public struct HostMetrics { +public struct HostMetrics: 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. @@ -1385,7 +1390,7 @@ public struct HostMetrics { /// /// Types of Measurements the telemetry module is equipped to handle -public struct Telemetry { +public struct Telemetry: @unchecked 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. @@ -1474,7 +1479,7 @@ public struct Telemetry { public var unknownFields = SwiftProtobuf.UnknownStorage() - public enum OneOf_Variant: Equatable { + public enum OneOf_Variant: Equatable, Sendable { /// /// Key native device metrics such as battery level case deviceMetrics(DeviceMetrics) @@ -1497,44 +1502,6 @@ public struct Telemetry { /// Linux host metrics case hostMetrics(HostMetrics) - #if !swift(>=4.1) - public static func ==(lhs: Telemetry.OneOf_Variant, rhs: Telemetry.OneOf_Variant) -> Bool { - // 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 (lhs, rhs) { - case (.deviceMetrics, .deviceMetrics): return { - guard case .deviceMetrics(let l) = lhs, case .deviceMetrics(let r) = rhs else { preconditionFailure() } - return l == r - }() - case (.environmentMetrics, .environmentMetrics): return { - guard case .environmentMetrics(let l) = lhs, case .environmentMetrics(let r) = rhs else { preconditionFailure() } - return l == r - }() - case (.airQualityMetrics, .airQualityMetrics): return { - guard case .airQualityMetrics(let l) = lhs, case .airQualityMetrics(let r) = rhs else { preconditionFailure() } - return l == r - }() - case (.powerMetrics, .powerMetrics): return { - guard case .powerMetrics(let l) = lhs, case .powerMetrics(let r) = rhs else { preconditionFailure() } - return l == r - }() - case (.localStats, .localStats): return { - guard case .localStats(let l) = lhs, case .localStats(let r) = rhs else { preconditionFailure() } - return l == r - }() - case (.healthMetrics, .healthMetrics): return { - guard case .healthMetrics(let l) = lhs, case .healthMetrics(let r) = rhs else { preconditionFailure() } - return l == r - }() - case (.hostMetrics, .hostMetrics): return { - guard case .hostMetrics(let l) = lhs, case .hostMetrics(let r) = rhs else { preconditionFailure() } - return l == r - }() - default: return false - } - } - #endif } public init() {} @@ -1544,7 +1511,7 @@ public struct Telemetry { /// /// NAU7802 Telemetry configuration, for saving to flash -public struct Nau7802Config { +public struct Nau7802Config: 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. @@ -1562,83 +1529,17 @@ public struct Nau7802Config { public init() {} } -#if swift(>=5.5) && canImport(_Concurrency) -extension TelemetrySensorType: @unchecked Sendable {} -extension DeviceMetrics: @unchecked Sendable {} -extension EnvironmentMetrics: @unchecked Sendable {} -extension PowerMetrics: @unchecked Sendable {} -extension AirQualityMetrics: @unchecked Sendable {} -extension LocalStats: @unchecked Sendable {} -extension HealthMetrics: @unchecked Sendable {} -extension HostMetrics: @unchecked Sendable {} -extension Telemetry: @unchecked Sendable {} -extension Telemetry.OneOf_Variant: @unchecked Sendable {} -extension Nau7802Config: @unchecked Sendable {} -#endif // swift(>=5.5) && canImport(_Concurrency) - // MARK: - Code below here is support for the SwiftProtobuf runtime. fileprivate let _protobuf_package = "meshtastic" extension TelemetrySensorType: SwiftProtobuf._ProtoNameProviding { - public static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ - 0: .same(proto: "SENSOR_UNSET"), - 1: .same(proto: "BME280"), - 2: .same(proto: "BME680"), - 3: .same(proto: "MCP9808"), - 4: .same(proto: "INA260"), - 5: .same(proto: "INA219"), - 6: .same(proto: "BMP280"), - 7: .same(proto: "SHTC3"), - 8: .same(proto: "LPS22"), - 9: .same(proto: "QMC6310"), - 10: .same(proto: "QMI8658"), - 11: .same(proto: "QMC5883L"), - 12: .same(proto: "SHT31"), - 13: .same(proto: "PMSA003I"), - 14: .same(proto: "INA3221"), - 15: .same(proto: "BMP085"), - 16: .same(proto: "RCWL9620"), - 17: .same(proto: "SHT4X"), - 18: .same(proto: "VEML7700"), - 19: .same(proto: "MLX90632"), - 20: .same(proto: "OPT3001"), - 21: .same(proto: "LTR390UV"), - 22: .same(proto: "TSL25911FN"), - 23: .same(proto: "AHT10"), - 24: .same(proto: "DFROBOT_LARK"), - 25: .same(proto: "NAU7802"), - 26: .same(proto: "BMP3XX"), - 27: .same(proto: "ICM20948"), - 28: .same(proto: "MAX17048"), - 29: .same(proto: "CUSTOM_SENSOR"), - 30: .same(proto: "MAX30102"), - 31: .same(proto: "MLX90614"), - 32: .same(proto: "SCD4X"), - 33: .same(proto: "RADSENS"), - 34: .same(proto: "INA226"), - 35: .same(proto: "DFROBOT_RAIN"), - 36: .same(proto: "DPS310"), - 37: .same(proto: "RAK12035"), - 38: .same(proto: "MAX17261"), - 39: .same(proto: "PCT2075"), - 40: .same(proto: "ADS1X15"), - 41: .same(proto: "ADS1X15_ALT"), - 42: .same(proto: "SFA30"), - 43: .same(proto: "SEN5X"), - 44: .same(proto: "TSL2561"), - ] + public static let _protobuf_nameMap = SwiftProtobuf._NameMap(bytecode: "\0\u{2}\0SENSOR_UNSET\0\u{1}BME280\0\u{1}BME680\0\u{1}MCP9808\0\u{1}INA260\0\u{1}INA219\0\u{1}BMP280\0\u{1}SHTC3\0\u{1}LPS22\0\u{1}QMC6310\0\u{1}QMI8658\0\u{1}QMC5883L\0\u{1}SHT31\0\u{1}PMSA003I\0\u{1}INA3221\0\u{1}BMP085\0\u{1}RCWL9620\0\u{1}SHT4X\0\u{1}VEML7700\0\u{1}MLX90632\0\u{1}OPT3001\0\u{1}LTR390UV\0\u{1}TSL25911FN\0\u{1}AHT10\0\u{1}DFROBOT_LARK\0\u{1}NAU7802\0\u{1}BMP3XX\0\u{1}ICM20948\0\u{1}MAX17048\0\u{1}CUSTOM_SENSOR\0\u{1}MAX30102\0\u{1}MLX90614\0\u{1}SCD4X\0\u{1}RADSENS\0\u{1}INA226\0\u{1}DFROBOT_RAIN\0\u{1}DPS310\0\u{1}RAK12035\0\u{1}MAX17261\0\u{1}PCT2075\0\u{1}ADS1X15\0\u{1}ADS1X15_ALT\0\u{1}SFA30\0\u{1}SEN5X\0\u{1}TSL2561\0\u{1}BH1750\0") } extension DeviceMetrics: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { public static let protoMessageName: String = _protobuf_package + ".DeviceMetrics" - public static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ - 1: .standard(proto: "battery_level"), - 2: .same(proto: "voltage"), - 3: .standard(proto: "channel_utilization"), - 4: .standard(proto: "air_util_tx"), - 5: .standard(proto: "uptime_seconds"), - ] + public static let _protobuf_nameMap = SwiftProtobuf._NameMap(bytecode: "\0\u{3}battery_level\0\u{1}voltage\0\u{3}channel_utilization\0\u{3}air_util_tx\0\u{3}uptime_seconds\0") public mutating func decodeMessage(decoder: inout D) throws { while let fieldNumber = try decoder.nextFieldNumber() { @@ -1692,30 +1593,7 @@ extension DeviceMetrics: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementa extension EnvironmentMetrics: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { public static let protoMessageName: String = _protobuf_package + ".EnvironmentMetrics" - public static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ - 1: .same(proto: "temperature"), - 2: .standard(proto: "relative_humidity"), - 3: .standard(proto: "barometric_pressure"), - 4: .standard(proto: "gas_resistance"), - 5: .same(proto: "voltage"), - 6: .same(proto: "current"), - 7: .same(proto: "iaq"), - 8: .same(proto: "distance"), - 9: .same(proto: "lux"), - 10: .standard(proto: "white_lux"), - 11: .standard(proto: "ir_lux"), - 12: .standard(proto: "uv_lux"), - 13: .standard(proto: "wind_direction"), - 14: .standard(proto: "wind_speed"), - 15: .same(proto: "weight"), - 16: .standard(proto: "wind_gust"), - 17: .standard(proto: "wind_lull"), - 18: .same(proto: "radiation"), - 19: .standard(proto: "rainfall_1h"), - 20: .standard(proto: "rainfall_24h"), - 21: .standard(proto: "soil_moisture"), - 22: .standard(proto: "soil_temperature"), - ] + public static let _protobuf_nameMap = SwiftProtobuf._NameMap(bytecode: "\0\u{1}temperature\0\u{3}relative_humidity\0\u{3}barometric_pressure\0\u{3}gas_resistance\0\u{1}voltage\0\u{1}current\0\u{1}iaq\0\u{1}distance\0\u{1}lux\0\u{3}white_lux\0\u{3}ir_lux\0\u{3}uv_lux\0\u{3}wind_direction\0\u{3}wind_speed\0\u{1}weight\0\u{3}wind_gust\0\u{3}wind_lull\0\u{1}radiation\0\u{3}rainfall_1h\0\u{3}rainfall_24h\0\u{3}soil_moisture\0\u{3}soil_temperature\0") fileprivate class _StorageClass { var _temperature: Float? = nil @@ -1741,15 +1619,11 @@ extension EnvironmentMetrics: SwiftProtobuf.Message, SwiftProtobuf._MessageImple var _soilMoisture: UInt32? = nil var _soilTemperature: Float? = nil - #if swift(>=5.10) // 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. // This will force a copy to be made of this reference when the first mutation occurs; // hence, it is safe to mark this as `nonisolated(unsafe)`. static nonisolated(unsafe) let defaultInstance = _StorageClass() - #else - static let defaultInstance = _StorageClass() - #endif private init() {} @@ -1936,24 +1810,7 @@ extension EnvironmentMetrics: SwiftProtobuf.Message, SwiftProtobuf._MessageImple extension PowerMetrics: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { public static let protoMessageName: String = _protobuf_package + ".PowerMetrics" - public static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ - 1: .standard(proto: "ch1_voltage"), - 2: .standard(proto: "ch1_current"), - 3: .standard(proto: "ch2_voltage"), - 4: .standard(proto: "ch2_current"), - 5: .standard(proto: "ch3_voltage"), - 6: .standard(proto: "ch3_current"), - 7: .standard(proto: "ch4_voltage"), - 8: .standard(proto: "ch4_current"), - 9: .standard(proto: "ch5_voltage"), - 10: .standard(proto: "ch5_current"), - 11: .standard(proto: "ch6_voltage"), - 12: .standard(proto: "ch6_current"), - 13: .standard(proto: "ch7_voltage"), - 14: .standard(proto: "ch7_current"), - 15: .standard(proto: "ch8_voltage"), - 16: .standard(proto: "ch8_current"), - ] + public static let _protobuf_nameMap = SwiftProtobuf._NameMap(bytecode: "\0\u{3}ch1_voltage\0\u{3}ch1_current\0\u{3}ch2_voltage\0\u{3}ch2_current\0\u{3}ch3_voltage\0\u{3}ch3_current\0\u{3}ch4_voltage\0\u{3}ch4_current\0\u{3}ch5_voltage\0\u{3}ch5_current\0\u{3}ch6_voltage\0\u{3}ch6_current\0\u{3}ch7_voltage\0\u{3}ch7_current\0\u{3}ch8_voltage\0\u{3}ch8_current\0") public mutating func decodeMessage(decoder: inout D) throws { while let fieldNumber = try decoder.nextFieldNumber() { @@ -2062,33 +1919,7 @@ extension PowerMetrics: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementat extension AirQualityMetrics: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { public static let protoMessageName: String = _protobuf_package + ".AirQualityMetrics" - public static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ - 1: .standard(proto: "pm10_standard"), - 2: .standard(proto: "pm25_standard"), - 3: .standard(proto: "pm100_standard"), - 4: .standard(proto: "pm10_environmental"), - 5: .standard(proto: "pm25_environmental"), - 6: .standard(proto: "pm100_environmental"), - 7: .standard(proto: "particles_03um"), - 8: .standard(proto: "particles_05um"), - 9: .standard(proto: "particles_10um"), - 10: .standard(proto: "particles_25um"), - 11: .standard(proto: "particles_50um"), - 12: .standard(proto: "particles_100um"), - 13: .same(proto: "co2"), - 14: .standard(proto: "co2_temperature"), - 15: .standard(proto: "co2_humidity"), - 16: .standard(proto: "form_formaldehyde"), - 17: .standard(proto: "form_humidity"), - 18: .standard(proto: "form_temperature"), - 19: .standard(proto: "pm40_standard"), - 20: .standard(proto: "particles_40um"), - 21: .standard(proto: "pm_temperature"), - 22: .standard(proto: "pm_humidity"), - 23: .standard(proto: "pm_voc_idx"), - 24: .standard(proto: "pm_nox_idx"), - 25: .standard(proto: "particles_tps"), - ] + public static let _protobuf_nameMap = SwiftProtobuf._NameMap(bytecode: "\0\u{3}pm10_standard\0\u{3}pm25_standard\0\u{3}pm100_standard\0\u{3}pm10_environmental\0\u{3}pm25_environmental\0\u{3}pm100_environmental\0\u{3}particles_03um\0\u{3}particles_05um\0\u{3}particles_10um\0\u{3}particles_25um\0\u{3}particles_50um\0\u{3}particles_100um\0\u{1}co2\0\u{3}co2_temperature\0\u{3}co2_humidity\0\u{3}form_formaldehyde\0\u{3}form_humidity\0\u{3}form_temperature\0\u{3}pm40_standard\0\u{3}particles_40um\0\u{3}pm_temperature\0\u{3}pm_humidity\0\u{3}pm_voc_idx\0\u{3}pm_nox_idx\0\u{3}particles_tps\0") fileprivate class _StorageClass { var _pm10Standard: UInt32? = nil @@ -2117,15 +1948,11 @@ extension AirQualityMetrics: SwiftProtobuf.Message, SwiftProtobuf._MessageImplem var _pmNoxIdx: Float? = nil var _particlesTps: Float? = nil - #if swift(>=5.10) // 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. // This will force a copy to be made of this reference when the first mutation occurs; // hence, it is safe to mark this as `nonisolated(unsafe)`. static nonisolated(unsafe) let defaultInstance = _StorageClass() - #else - static let defaultInstance = _StorageClass() - #endif private init() {} @@ -2330,21 +2157,7 @@ extension AirQualityMetrics: SwiftProtobuf.Message, SwiftProtobuf._MessageImplem extension LocalStats: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { public static let protoMessageName: String = _protobuf_package + ".LocalStats" - public static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ - 1: .standard(proto: "uptime_seconds"), - 2: .standard(proto: "channel_utilization"), - 3: .standard(proto: "air_util_tx"), - 4: .standard(proto: "num_packets_tx"), - 5: .standard(proto: "num_packets_rx"), - 6: .standard(proto: "num_packets_rx_bad"), - 7: .standard(proto: "num_online_nodes"), - 8: .standard(proto: "num_total_nodes"), - 9: .standard(proto: "num_rx_dupe"), - 10: .standard(proto: "num_tx_relay"), - 11: .standard(proto: "num_tx_relay_canceled"), - 12: .standard(proto: "heap_total_bytes"), - 13: .standard(proto: "heap_free_bytes"), - ] + public static let _protobuf_nameMap = SwiftProtobuf._NameMap(bytecode: "\0\u{3}uptime_seconds\0\u{3}channel_utilization\0\u{3}air_util_tx\0\u{3}num_packets_tx\0\u{3}num_packets_rx\0\u{3}num_packets_rx_bad\0\u{3}num_online_nodes\0\u{3}num_total_nodes\0\u{3}num_rx_dupe\0\u{3}num_tx_relay\0\u{3}num_tx_relay_canceled\0\u{3}heap_total_bytes\0\u{3}heap_free_bytes\0\u{3}num_tx_dropped\0") public mutating func decodeMessage(decoder: inout D) throws { while let fieldNumber = try decoder.nextFieldNumber() { @@ -2365,6 +2178,7 @@ extension LocalStats: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementatio case 11: try { try decoder.decodeSingularUInt32Field(value: &self.numTxRelayCanceled) }() case 12: try { try decoder.decodeSingularUInt32Field(value: &self.heapTotalBytes) }() case 13: try { try decoder.decodeSingularUInt32Field(value: &self.heapFreeBytes) }() + case 14: try { try decoder.decodeSingularUInt32Field(value: &self.numTxDropped) }() default: break } } @@ -2374,10 +2188,10 @@ extension LocalStats: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementatio if self.uptimeSeconds != 0 { try visitor.visitSingularUInt32Field(value: self.uptimeSeconds, fieldNumber: 1) } - if self.channelUtilization != 0 { + if self.channelUtilization.bitPattern != 0 { try visitor.visitSingularFloatField(value: self.channelUtilization, fieldNumber: 2) } - if self.airUtilTx != 0 { + if self.airUtilTx.bitPattern != 0 { try visitor.visitSingularFloatField(value: self.airUtilTx, fieldNumber: 3) } if self.numPacketsTx != 0 { @@ -2410,6 +2224,9 @@ extension LocalStats: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementatio if self.heapFreeBytes != 0 { try visitor.visitSingularUInt32Field(value: self.heapFreeBytes, fieldNumber: 13) } + if self.numTxDropped != 0 { + try visitor.visitSingularUInt32Field(value: self.numTxDropped, fieldNumber: 14) + } try unknownFields.traverse(visitor: &visitor) } @@ -2427,6 +2244,7 @@ extension LocalStats: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementatio if lhs.numTxRelayCanceled != rhs.numTxRelayCanceled {return false} if lhs.heapTotalBytes != rhs.heapTotalBytes {return false} if lhs.heapFreeBytes != rhs.heapFreeBytes {return false} + if lhs.numTxDropped != rhs.numTxDropped {return false} if lhs.unknownFields != rhs.unknownFields {return false} return true } @@ -2434,11 +2252,7 @@ extension LocalStats: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementatio extension HealthMetrics: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { public static let protoMessageName: String = _protobuf_package + ".HealthMetrics" - public static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ - 1: .standard(proto: "heart_bpm"), - 2: .same(proto: "spO2"), - 3: .same(proto: "temperature"), - ] + public static let _protobuf_nameMap = SwiftProtobuf._NameMap(bytecode: "\0\u{3}heart_bpm\0\u{1}spO2\0\u{1}temperature\0") public mutating func decodeMessage(decoder: inout D) throws { while let fieldNumber = try decoder.nextFieldNumber() { @@ -2482,17 +2296,7 @@ extension HealthMetrics: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementa extension HostMetrics: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { public static let protoMessageName: String = _protobuf_package + ".HostMetrics" - public static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ - 1: .standard(proto: "uptime_seconds"), - 2: .standard(proto: "freemem_bytes"), - 3: .standard(proto: "diskfree1_bytes"), - 4: .standard(proto: "diskfree2_bytes"), - 5: .standard(proto: "diskfree3_bytes"), - 6: .same(proto: "load1"), - 7: .same(proto: "load5"), - 8: .same(proto: "load15"), - 9: .standard(proto: "user_string"), - ] + public static let _protobuf_nameMap = SwiftProtobuf._NameMap(bytecode: "\0\u{3}uptime_seconds\0\u{3}freemem_bytes\0\u{3}diskfree1_bytes\0\u{3}diskfree2_bytes\0\u{3}diskfree3_bytes\0\u{1}load1\0\u{1}load5\0\u{1}load15\0\u{3}user_string\0") public mutating func decodeMessage(decoder: inout D) throws { while let fieldNumber = try decoder.nextFieldNumber() { @@ -2566,30 +2370,17 @@ extension HostMetrics: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementati extension Telemetry: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { public static let protoMessageName: String = _protobuf_package + ".Telemetry" - public static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ - 1: .same(proto: "time"), - 2: .standard(proto: "device_metrics"), - 3: .standard(proto: "environment_metrics"), - 4: .standard(proto: "air_quality_metrics"), - 5: .standard(proto: "power_metrics"), - 6: .standard(proto: "local_stats"), - 7: .standard(proto: "health_metrics"), - 8: .standard(proto: "host_metrics"), - ] + public static let _protobuf_nameMap = SwiftProtobuf._NameMap(bytecode: "\0\u{1}time\0\u{3}device_metrics\0\u{3}environment_metrics\0\u{3}air_quality_metrics\0\u{3}power_metrics\0\u{3}local_stats\0\u{3}health_metrics\0\u{3}host_metrics\0") fileprivate class _StorageClass { var _time: UInt32 = 0 var _variant: Telemetry.OneOf_Variant? - #if swift(>=5.10) // 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. // This will force a copy to be made of this reference when the first mutation occurs; // hence, it is safe to mark this as `nonisolated(unsafe)`. static nonisolated(unsafe) let defaultInstance = _StorageClass() - #else - static let defaultInstance = _StorageClass() - #endif private init() {} @@ -2774,10 +2565,7 @@ extension Telemetry: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementation extension Nau7802Config: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { public static let protoMessageName: String = _protobuf_package + ".Nau7802Config" - public static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ - 1: .same(proto: "zeroOffset"), - 2: .same(proto: "calibrationFactor"), - ] + public static let _protobuf_nameMap = SwiftProtobuf._NameMap(bytecode: "\0\u{1}zeroOffset\0\u{1}calibrationFactor\0") public mutating func decodeMessage(decoder: inout D) throws { while let fieldNumber = try decoder.nextFieldNumber() { @@ -2796,7 +2584,7 @@ extension Nau7802Config: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementa if self.zeroOffset != 0 { try visitor.visitSingularInt32Field(value: self.zeroOffset, fieldNumber: 1) } - if self.calibrationFactor != 0 { + if self.calibrationFactor.bitPattern != 0 { try visitor.visitSingularFloatField(value: self.calibrationFactor, fieldNumber: 2) } try unknownFields.traverse(visitor: &visitor) diff --git a/MeshtasticProtobufs/Sources/meshtastic/xmodem.pb.swift b/MeshtasticProtobufs/Sources/meshtastic/xmodem.pb.swift index 1f41fe0b..010c6eed 100644 --- a/MeshtasticProtobufs/Sources/meshtastic/xmodem.pb.swift +++ b/MeshtasticProtobufs/Sources/meshtastic/xmodem.pb.swift @@ -1,5 +1,6 @@ // DO NOT EDIT. // swift-format-ignore-file +// swiftlint:disable all // // Generated by the Swift generator plugin for the protocol buffer compiler. // Source: meshtastic/xmodem.proto @@ -20,7 +21,7 @@ fileprivate struct _GeneratedWithProtocGenSwiftVersion: SwiftProtobuf.ProtobufAP typealias Version = _2 } -public struct XModem { +public struct XModem: 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. @@ -35,7 +36,7 @@ public struct XModem { public var unknownFields = SwiftProtobuf.UnknownStorage() - public enum Control: SwiftProtobuf.Enum { + public enum Control: SwiftProtobuf.Enum, Swift.CaseIterable { public typealias RawValue = Int case nul // = 0 case soh // = 1 @@ -79,46 +80,30 @@ public struct XModem { } } + // The compiler won't synthesize support with the UNRECOGNIZED case. + public static let allCases: [XModem.Control] = [ + .nul, + .soh, + .stx, + .eot, + .ack, + .nak, + .can, + .ctrlz, + ] + } public init() {} } -#if swift(>=4.2) - -extension XModem.Control: CaseIterable { - // The compiler won't synthesize support with the UNRECOGNIZED case. - public static let allCases: [XModem.Control] = [ - .nul, - .soh, - .stx, - .eot, - .ack, - .nak, - .can, - .ctrlz, - ] -} - -#endif // swift(>=4.2) - -#if swift(>=5.5) && canImport(_Concurrency) -extension XModem: @unchecked Sendable {} -extension XModem.Control: @unchecked Sendable {} -#endif // swift(>=5.5) && canImport(_Concurrency) - // MARK: - Code below here is support for the SwiftProtobuf runtime. fileprivate let _protobuf_package = "meshtastic" extension XModem: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { public static let protoMessageName: String = _protobuf_package + ".XModem" - public static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ - 1: .same(proto: "control"), - 2: .same(proto: "seq"), - 3: .same(proto: "crc16"), - 4: .same(proto: "buffer"), - ] + public static let _protobuf_nameMap = SwiftProtobuf._NameMap(bytecode: "\0\u{1}control\0\u{1}seq\0\u{1}crc16\0\u{1}buffer\0") public mutating func decodeMessage(decoder: inout D) throws { while let fieldNumber = try decoder.nextFieldNumber() { @@ -162,14 +147,5 @@ extension XModem: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBas } extension XModem.Control: SwiftProtobuf._ProtoNameProviding { - public static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ - 0: .same(proto: "NUL"), - 1: .same(proto: "SOH"), - 2: .same(proto: "STX"), - 4: .same(proto: "EOT"), - 6: .same(proto: "ACK"), - 21: .same(proto: "NAK"), - 24: .same(proto: "CAN"), - 26: .same(proto: "CTRLZ"), - ] + public static let _protobuf_nameMap = SwiftProtobuf._NameMap(bytecode: "\0\u{2}\0NUL\0\u{1}SOH\0\u{1}STX\0\u{2}\u{2}EOT\0\u{2}\u{2}ACK\0\u{2}\u{f}NAK\0\u{2}\u{3}CAN\0\u{2}\u{2}CTRLZ\0") } diff --git a/protobufs b/protobufs index c1e31a96..62ef17b3 160000 --- a/protobufs +++ b/protobufs @@ -1 +1 @@ -Subproject commit c1e31a9655e9920a8b5b8eccdf7c69ef1ae42a49 +Subproject commit 62ef17b3d1625fc6d78ed661f614d0baad4be9ef From 34794d8b2266b87d963fd36fc655977d2553afbc Mon Sep 17 00:00:00 2001 From: Ben Meadors Date: Wed, 17 Dec 2025 20:34:47 -0600 Subject: [PATCH 48/68] Add long-turbo preset --- Meshtastic/Enums/LoraConfigEnums.swift | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/Meshtastic/Enums/LoraConfigEnums.swift b/Meshtastic/Enums/LoraConfigEnums.swift index b196a7a7..7be12c7c 100644 --- a/Meshtastic/Enums/LoraConfigEnums.swift +++ b/Meshtastic/Enums/LoraConfigEnums.swift @@ -313,6 +313,7 @@ enum ModemPresets: Int, CaseIterable, Identifiable { case longFast = 0 case longSlow = 1 case longModerate = 7 + case longTurbo = 9 case medSlow = 3 case medFast = 4 case shortSlow = 5 @@ -328,6 +329,8 @@ enum ModemPresets: Int, CaseIterable, Identifiable { return "Long Range - Slow".localized case .longModerate: return "Long Range - Moderate".localized + case .longTurbo: + return "Long Range - Turbo".localized case .medSlow: return "Medium Range - Slow".localized case .medFast: @@ -348,6 +351,8 @@ enum ModemPresets: Int, CaseIterable, Identifiable { return "LongSlow" case .longModerate: return "LongModerate" + case .longTurbo: + return "LongTurbo" case .medSlow: return "MediumSlow" case .medFast: @@ -366,6 +371,8 @@ enum ModemPresets: Int, CaseIterable, Identifiable { return -17.5 case .longSlow: return -7.5 + case .longTurbo: + return -12.5 case .longModerate: return -17.5 case .medSlow: @@ -388,6 +395,8 @@ enum ModemPresets: Int, CaseIterable, Identifiable { return Config.LoRaConfig.ModemPreset.longSlow case .longModerate: return Config.LoRaConfig.ModemPreset.longModerate + case .longTurbo: + return Config.LoRaConfig.ModemPreset.longTurbo case .medSlow: return Config.LoRaConfig.ModemPreset.mediumSlow case .medFast: From ffcbeee8f1871052ec2ae8489d47171c71c660e8 Mon Sep 17 00:00:00 2001 From: Jason Houk Date: Wed, 17 Dec 2025 23:01:36 -0500 Subject: [PATCH 49/68] Disable Range Test module when primary channel is public/unsecured (#1512) * Update Muzi R1 Neo to actively supported * Disable Range Test module when primary channel is public/unsecured Updated RangeTestConfig.swift to determine whether the primary channel (index 0) is operating without encryption or with a 1-byte minimal PSK. Disabled Range Test UI controls when on a public/default channel to prevent user interaction. Added safety enforcement in the save operation: Range Test enabled flag is automatically forced to false before sending updates to the device. Introduced a computed property isPrimaryChannelPublic following existing code patterns and security indicators (e.g., hexDescription PSK length). Matches the behavior implemented in the Android client for consistent policy across platforms. --------- Co-authored-by: Jonathan Bennett Co-authored-by: Ben Meadors --- .../Config/Module/RangeTestConfig.swift | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) diff --git a/Meshtastic/Views/Settings/Config/Module/RangeTestConfig.swift b/Meshtastic/Views/Settings/Config/Module/RangeTestConfig.swift index a4bfbd85..ed65ce4e 100644 --- a/Meshtastic/Views/Settings/Config/Module/RangeTestConfig.swift +++ b/Meshtastic/Views/Settings/Config/Module/RangeTestConfig.swift @@ -5,6 +5,7 @@ // Copyright (c) Garth Vander Houwen 6/13/22. // import MeshtasticProtobufs +import CoreData import OSLog import SwiftUI @@ -21,6 +22,19 @@ struct RangeTestConfig: View { @State var enabled = false @State var save = false @State private var sender: UpdateInterval = UpdateInterval(from: 0) + private var isPrimaryChannelPublic: Bool { + guard let channels = node?.myInfo?.channels?.array as? [ChannelEntity] else { + return false + } + // Treat the primary channel on this node as "public" when it is effectively unencrypted + // or using a minimal 1-byte key (hexDescription shorter than 3 characters). + guard let primary = channels.first(where: { $0.index == 0 && $0.role > 0 }) else { + return false + } + let hexLen = primary.psk?.hexDescription.count ?? 0 + return hexLen < 3 + } + var body: some View { Form { @@ -51,14 +65,15 @@ struct RangeTestConfig: View { } } - .disabled(!accessoryManager.isConnected || node?.rangeTestConfig == nil) + .disabled(!accessoryManager.isConnected || node?.rangeTestConfig == nil || isPrimaryChannelPublic) .safeAreaInset(edge: .bottom, alignment: .center) { HStack(spacing: 0) { SaveConfigButton(node: node, hasChanges: $hasChanges) { let connectedNode = getNodeInfo(id: accessoryManager.activeDeviceNum ?? -1, context: context) if connectedNode != nil { var rtc = ModuleConfig.RangeTestConfig() - rtc.enabled = enabled + let effectiveEnabled = isPrimaryChannelPublic ? false : enabled + rtc.enabled = effectiveEnabled rtc.save = save rtc.sender = UInt32(sender.intValue) Task { From 8e27d9ad74176c2283ff31e4691f86bd245c04dd Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Sun, 21 Dec 2025 09:24:50 -0800 Subject: [PATCH 50/68] Add new device images --- .../HELTECV4.imageset/Contents.json | 12 +++ .../HELTECV4.imageset/heltec_v4.svg | 1 + .../THINKNODEM3.imageset/Contents.json | 12 +++ .../THINKNODEM3.imageset/thinknode_m3.svg | 1 + .../THINKNODEM4.imageset/Contents.json | 12 +++ .../THINKNODEM4.imageset/thinknode_m4.svg | 1 + .../CoreData/UserEntityExtension.swift | 11 +++ Meshtastic/Resources/DeviceHardware.json | 96 ++++++++++++++++++- 8 files changed, 142 insertions(+), 4 deletions(-) create mode 100644 Meshtastic/Assets.xcassets/HELTECV4.imageset/Contents.json create mode 100644 Meshtastic/Assets.xcassets/HELTECV4.imageset/heltec_v4.svg create mode 100644 Meshtastic/Assets.xcassets/THINKNODEM3.imageset/Contents.json create mode 100644 Meshtastic/Assets.xcassets/THINKNODEM3.imageset/thinknode_m3.svg create mode 100644 Meshtastic/Assets.xcassets/THINKNODEM4.imageset/Contents.json create mode 100644 Meshtastic/Assets.xcassets/THINKNODEM4.imageset/thinknode_m4.svg diff --git a/Meshtastic/Assets.xcassets/HELTECV4.imageset/Contents.json b/Meshtastic/Assets.xcassets/HELTECV4.imageset/Contents.json new file mode 100644 index 00000000..a90a896f --- /dev/null +++ b/Meshtastic/Assets.xcassets/HELTECV4.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "heltec_v4.svg", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Meshtastic/Assets.xcassets/HELTECV4.imageset/heltec_v4.svg b/Meshtastic/Assets.xcassets/HELTECV4.imageset/heltec_v4.svg new file mode 100644 index 00000000..849d056f --- /dev/null +++ b/Meshtastic/Assets.xcassets/HELTECV4.imageset/heltec_v4.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/Meshtastic/Assets.xcassets/THINKNODEM3.imageset/Contents.json b/Meshtastic/Assets.xcassets/THINKNODEM3.imageset/Contents.json new file mode 100644 index 00000000..625b6450 --- /dev/null +++ b/Meshtastic/Assets.xcassets/THINKNODEM3.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "thinknode_m3.svg", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Meshtastic/Assets.xcassets/THINKNODEM3.imageset/thinknode_m3.svg b/Meshtastic/Assets.xcassets/THINKNODEM3.imageset/thinknode_m3.svg new file mode 100644 index 00000000..ce5ce6fc --- /dev/null +++ b/Meshtastic/Assets.xcassets/THINKNODEM3.imageset/thinknode_m3.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/Meshtastic/Assets.xcassets/THINKNODEM4.imageset/Contents.json b/Meshtastic/Assets.xcassets/THINKNODEM4.imageset/Contents.json new file mode 100644 index 00000000..2c2709dc --- /dev/null +++ b/Meshtastic/Assets.xcassets/THINKNODEM4.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "thinknode_m4.svg", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Meshtastic/Assets.xcassets/THINKNODEM4.imageset/thinknode_m4.svg b/Meshtastic/Assets.xcassets/THINKNODEM4.imageset/thinknode_m4.svg new file mode 100644 index 00000000..a5050859 --- /dev/null +++ b/Meshtastic/Assets.xcassets/THINKNODEM4.imageset/thinknode_m4.svg @@ -0,0 +1 @@ +T&HStatusFunction \ No newline at end of file diff --git a/Meshtastic/Extensions/CoreData/UserEntityExtension.swift b/Meshtastic/Extensions/CoreData/UserEntityExtension.swift index fbf55f5a..c5c3112c 100644 --- a/Meshtastic/Extensions/CoreData/UserEntityExtension.swift +++ b/Meshtastic/Extensions/CoreData/UserEntityExtension.swift @@ -76,6 +76,8 @@ extension UserEntity { return "HELTECMESHNODET114" case "HELTECV3": return "HELTECV3" + case "HELTECV4": + return "HELTECV4" case "HELTECMESHPOCKET": return "HELTECMESHPOCKET" case "HELTECVISIONMASTERE213": @@ -139,6 +141,15 @@ extension UserEntity { return "MUZIR1NEO" case "STATIONG2": return "STATIONG2" + /// Elecrow + case "THINKNODEM1": + return "THINKNODEM1" + case "THINKNODEM2": + return "THINKNODEM2" + case "THINKNODEM3": + return "THINKNODEM3" + case "THINKNODEM3": + return "THINKNODEM4" /// DIY Devices case "RPIPICO": return "RPIPICO" diff --git a/Meshtastic/Resources/DeviceHardware.json b/Meshtastic/Resources/DeviceHardware.json index a49e973e..3db1437d 100644 --- a/Meshtastic/Resources/DeviceHardware.json +++ b/Meshtastic/Resources/DeviceHardware.json @@ -908,6 +908,22 @@ "thinknode_m2.svg" ] }, + { + "hwModel": 93, + "hwModelSlug": "MUZI_BASE", + "platformioTarget": "muzi-base", + "architecture": "nrf52840", + "activelySupported": false, + "supportLevel": 1, + "displayName": "muzi base", + "tags": [ + "muzi" + ], + "requiresDfu": true, + "images": [ + "muzi_base.svg" + ] + }, { "hwModel": 94, "hwModelSlug": "HELTEC_MESH_POCKET", @@ -923,7 +939,28 @@ "heltec_mesh_pocket.svg" ], "requiresDfu": true, - "hasInkHud": true + "hasInkHud": true, + "key": "HELTEC_MESH_POCKET", + "variant": "10000mAh" + }, + { + "hwModel": 94, + "hwModelSlug": "HELTEC_MESH_POCKET", + "platformioTarget": "heltec-mesh-pocket-5000", + "architecture": "nrf52840", + "activelySupported": true, + "supportLevel": 1, + "displayName": "Heltec MeshPocket", + "tags": [ + "Heltec" + ], + "images": [ + "heltec_mesh_pocket.svg" + ], + "requiresDfu": true, + "hasInkHud": true, + "key": "HELTEC_MESH_POCKET", + "variant": "5000mAh" }, { "hwModel": 95, @@ -1187,13 +1224,64 @@ "hwModelSlug": "THINKNODE_M3", "platformioTarget": "thinknode_m3", "architecture": "nrf52840", - "activelySupported": false, + "activelySupported": true, "supportLevel": 1, "displayName": "ThinkNode M3", "tags": [ "Elecrow" ], - "requiresDfu": true + "requiresDfu": true, + "images": [ + "thinknode_m3.svg" + ] + }, + { + "hwModel": 116, + "hwModelSlug": "WISMESH_TAP_V2", + "platformioTarget": "rak_wismesh_tap_v2", + "architecture": "esp32-s3", + "activelySupported": false, + "supportLevel": 1, + "displayName": "RAK WisMesh Tap V2", + "tags": [ + "RAK" + ], + "hasMui": true, + "partitionScheme": "8MB", + "images": [ + "rak-wismesh-tap-v2.svg" + ] + }, + { + "hwModel": 119, + "hwModelSlug": "THINKNODE_M4", + "platformioTarget": "thinknode_m4", + "architecture": "nrf52840", + "activelySupported": false, + "supportLevel": 1, + "displayName": "ThinkNode M4", + "tags": [ + "Elecrow" + ], + "requiresDfu": true, + "images": [ + "thinknode_m4.svg" + ] + }, + { + "hwModel": 120, + "hwModelSlug": "THINKNODE_M6", + "platformioTarget": "thinknode_m6", + "architecture": "nrf52840", + "activelySupported": false, + "supportLevel": 1, + "displayName": "ThinkNode M6", + "tags": [ + "Elecrow" + ], + "requiresDfu": true, + "images": [ + "thinknode_m6.svg" + ] } ] - From 6e50872df37c11c8ae961dbb3db99f65a301d751 Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Sun, 21 Dec 2025 12:10:55 -0800 Subject: [PATCH 51/68] Remove print statement, get rid of cut and paste error --- Meshtastic/Extensions/CoreData/UserEntityExtension.swift | 2 +- Meshtastic/Extensions/UserDefaults.swift | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/Meshtastic/Extensions/CoreData/UserEntityExtension.swift b/Meshtastic/Extensions/CoreData/UserEntityExtension.swift index c5c3112c..3a61ff4b 100644 --- a/Meshtastic/Extensions/CoreData/UserEntityExtension.swift +++ b/Meshtastic/Extensions/CoreData/UserEntityExtension.swift @@ -148,7 +148,7 @@ extension UserEntity { return "THINKNODEM2" case "THINKNODEM3": return "THINKNODEM3" - case "THINKNODEM3": + case "THINKNODEM4": return "THINKNODEM4" /// DIY Devices case "RPIPICO": diff --git a/Meshtastic/Extensions/UserDefaults.swift b/Meshtastic/Extensions/UserDefaults.swift index 751ddc17..82e67773 100644 --- a/Meshtastic/Extensions/UserDefaults.swift +++ b/Meshtastic/Extensions/UserDefaults.swift @@ -6,6 +6,7 @@ // import Foundation +import OSLog @propertyWrapper struct UserDefault { @@ -205,7 +206,7 @@ extension UserDefaults { // Store the Data in UserDefaults UserDefaults.standard.set(data, forKey: Keys.manualConnections.rawValue) } catch { - print("Failed to encode manualConnections: \(error)") + Logger.transport.error("💥 Failed to encode manualConnections: \(error, privacy: .public)") } } } From b2163877d038f9eef59fd1854fa3d685a960114c Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Sun, 4 Jan 2026 20:06:03 -0800 Subject: [PATCH 52/68] 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 b87ce5f2..cd52c496 100644 --- a/Meshtastic.xcodeproj/project.pbxproj +++ b/Meshtastic.xcodeproj/project.pbxproj @@ -2102,7 +2102,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 +2137,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 +2169,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 +2202,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 = ""; From cb2fd8cc15185f6b9ce8a940d8ca8d11a32a2f80 Mon Sep 17 00:00:00 2001 From: MGJ <62177301+MGJ520@users.noreply.github.com> Date: Mon, 5 Jan 2026 12:16:16 +0800 Subject: [PATCH 53/68] update the translations (#1540) update the translations --- Localizable.xcstrings | 120 ++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 117 insertions(+), 3 deletions(-) diff --git a/Localizable.xcstrings b/Localizable.xcstrings index 33be032d..e4f5e806 100644 --- a/Localizable.xcstrings +++ b/Localizable.xcstrings @@ -2114,6 +2114,12 @@ "value" : "О Мештастику" } }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "关于" + } + }, "zh-Hant-TW" : { "stringUnit" : { "state" : "translated", @@ -7604,6 +7610,12 @@ "value" : "Очисти логове" } }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "清除Log记录" + } + }, "zh-Hant-TW" : { "stringUnit" : { "state" : "translated", @@ -8322,7 +8334,13 @@ "state" : "translated", "value" : "Повежи се" } - } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "连接" + } + }, } }, "Connect to a Node" : { @@ -10332,6 +10350,12 @@ "value" : "Логови сензора откривања" } }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "检测传感器记录" + } + }, "zh-Hant-TW" : { "stringUnit" : { "state" : "translated", @@ -14617,6 +14641,12 @@ "value" : "Пронађи контакт" } }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "搜索联系人" + } + }, "zh-Hant-TW" : { "stringUnit" : { "state" : "translated", @@ -14651,6 +14681,12 @@ "value" : "Пронађи чвор" } }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "搜索节点" + } + }, "zh-Hant-TW" : { "stringUnit" : { "state" : "translated", @@ -14938,6 +14974,12 @@ "value" : "Прво откривање" } }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "首次通信" + } + }, "zh-Hant-TW" : { "stringUnit" : { "state" : "translated", @@ -15930,6 +15972,12 @@ "value" : "Апсолутна подршка" } }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "官方支持" + } + }, "zh-Hant-TW" : { "stringUnit" : { "state" : "translated", @@ -16880,6 +16928,12 @@ "value" : "Сакриј упозорења" } }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "通知静音" + } + }, "zh-Hant-TW" : { "stringUnit" : { "state" : "translated", @@ -20388,7 +20442,13 @@ "state" : "translated", "value" : "Ручно" } - } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "更多" + } + }, } }, "Manual Configuration" : { @@ -20915,7 +20975,7 @@ "zh-Hans" : { "stringUnit" : { "state" : "translated", - "value" : "Mesh 地图" + "value" : "地图" } }, "zh-Hant-TW" : { @@ -23772,6 +23832,12 @@ "value" : "Број чвора" } }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "用户编号" + } + }, "zh-Hant-TW" : { "stringUnit" : { "state" : "translated", @@ -26585,6 +26651,12 @@ "value" : "Логови позиција" } }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "位置记录" + } + }, "zh-Hant-TW" : { "stringUnit" : { "state" : "translated", @@ -26949,6 +27021,12 @@ "value" : "Логови метрике снаге" } }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "电源指标记录" + } + }, "zh-Hant-TW" : { "stringUnit" : { "state" : "translated", @@ -29929,6 +30007,12 @@ "value" : "Улога" } }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "节点类型" + } + }, "zh-Hant-TW" : { "stringUnit" : { "state" : "translated", @@ -30165,6 +30249,12 @@ "value" : "Снимач руте" } }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "路线记录" + } + }, "zh-Hant-TW" : { "stringUnit" : { "state" : "translated", @@ -30341,6 +30431,12 @@ "value" : "Руте" } }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "路线" + } + }, "zh-Hant-TW" : { "stringUnit" : { "state" : "translated", @@ -31557,6 +31653,12 @@ "value" : "Изабери тип разговора" } }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "请选择对话类型" + } + }, "zh-Hant-TW" : { "stringUnit" : { "state" : "translated", @@ -37990,6 +38092,12 @@ "value" : "Лог праћења руте комуникације" } }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "追踪路由(Trace Route)记录" + } + }, "zh-Hant-TW" : { "stringUnit" : { "state" : "translated", @@ -39802,6 +39910,12 @@ "value" : "Време рада" } }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "上线时长" + } + }, "zh-Hant-TW" : { "stringUnit" : { "state" : "translated", From 79fac42097f7d7ebf7a7c24fb0a96191397a5f1e Mon Sep 17 00:00:00 2001 From: Mike Robbins Date: Sun, 4 Jan 2026 20:17:56 -0800 Subject: [PATCH 54/68] Don't alert (with sound: .default) when updating Live Activity (#1536) --- Meshtastic/Helpers/MeshPackets.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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.") } } From bff8ca018ba1b673ceb91ecd80b706b25c707b88 Mon Sep 17 00:00:00 2001 From: Benjamin Faershtein <119711889+RCGV1@users.noreply.github.com> Date: Sun, 4 Jan 2026 20:21:15 -0800 Subject: [PATCH 55/68] Fix adding channels (#1532) --- .../AccessoryManager+ToRadio.swift | 78 ++++++++++--------- 1 file changed, 43 insertions(+), 35 deletions(-) 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).. Date: Sun, 4 Jan 2026 23:22:58 -0500 Subject: [PATCH 56/68] Full translation into Spanish (#1529) --- Localizable.xcstrings | 7024 ++++++++++++++++++++++++++++++++++++++++- 1 file changed, 7013 insertions(+), 11 deletions(-) diff --git a/Localizable.xcstrings b/Localizable.xcstrings index e4f5e806..78c9454b 100644 --- a/Localizable.xcstrings +++ b/Localizable.xcstrings @@ -6,6 +6,12 @@ }, "\t%@" : { "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "\t%@" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -41,6 +47,12 @@ }, " %@" : { "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : " %@" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -134,6 +146,12 @@ }, ": %d" : { "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : ": %d" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -169,6 +187,12 @@ }, "(Re)define PIN_GPS_EN for your board." : { "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "(Re)defina PIN_GPS_EN para su placa." + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -203,6 +227,12 @@ }, "%@" : { "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "%@" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -326,6 +356,12 @@ "value" : "%1$@ - %2$@ Towards %3$@ Back" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "%@ - %@ Hacia %@ Atrás" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -366,6 +402,12 @@ "value" : "%@ - Keine Antwort" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "%@ - Sin respuesta" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -406,6 +448,12 @@ "value" : "%@ - Nicht gesendet" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "%@ - No enviado" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -528,6 +576,12 @@ "value" : "%1$@ %2$lld" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "%@ %lld" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -569,6 +623,12 @@ "value" : "%@ entfernt" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "%@ lejos" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -609,6 +669,12 @@ "value" : "%@ kann bis zu %@ Byte lang sein." } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "%@ puede tener hasta %@ bytes de longitud." + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -643,6 +709,12 @@ }, "%@ Channels?" : { "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "%@ ¿Canales?" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -677,6 +749,12 @@ }, "%@ config data was requested via PKC admin but no response has been returned from the remote node." : { "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Se solicitaron %@ datos de configuración a través del administrador de PKC pero no se obtuvo respuesta del nodo remoto." + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -687,6 +765,12 @@ }, "%@ dB" : { "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "%@dB" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -768,6 +852,12 @@ "value" : "%1$@: %2$lld / %3$lld" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "%@: %lld / %lld" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -803,6 +893,12 @@ }, "%@%%" : { "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "%@%%" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -837,6 +933,12 @@ }, "%@°F" : { "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "%@°F" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -871,6 +973,12 @@ }, "%@mA" : { "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "%@mA" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -899,6 +1007,12 @@ }, "%@V" : { "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "%@V" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -927,6 +1041,12 @@ }, "%d" : { "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "%d" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -979,6 +1099,12 @@ } } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "%d saltos" + } + }, "it" : { "variations" : { "plural" : { @@ -1061,6 +1187,12 @@ }, "%d%%" : { "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "%d%%" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -1095,6 +1227,12 @@ }, "%f%%" : { "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "%f%%" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -1105,6 +1243,12 @@ }, "%lf" : { "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "%lf" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -1139,6 +1283,12 @@ }, "%lld" : { "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "%lld" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -1179,6 +1329,12 @@ "value" : "%1$lld %2$@" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "%lld %@" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -1225,6 +1381,12 @@ } } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "%lld características" + } + }, "sr" : { "variations" : { "plural" : { @@ -1259,6 +1421,12 @@ "value" : "%lld oder weniger Hops entfernt" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "%lld o menos salta de distancia" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -1287,6 +1455,12 @@ }, "%lld Readings Total" : { "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "%lld lecturas totales" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -1315,6 +1489,12 @@ }, "%lld Total Detection Events" : { "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "%lld total de eventos de detección" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -1343,6 +1523,12 @@ }, "%lld%%" : { "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "%lld%%" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -1383,6 +1569,12 @@ "value" : "%llddb Übertragungsleistung" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "%llddb Potencia de transmisión" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -1423,6 +1615,12 @@ "value" : "%llddBm Übertragungsleistung" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "%llddBm Potencia de transmisión" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -1454,6 +1652,12 @@ }, "< 1%" : { "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "< 1%" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -1486,12 +1690,32 @@ } } }, +<<<<<<< Updated upstream "⚠️ The configured value: (%@) is not one of the optimized options." : { "comment" : "A warning label below the picker, indicating that the selected update interval is not one of the optimized options.", "isCommentAutoGenerated" : true +======= + "⚠️ The configured value: (%@ seconds) is not one of the optimized options." : { + "comment" : "A text warning that the configured update interval is not one of the optimized options.", + "isCommentAutoGenerated" : true, + "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "⚠️ El valor configurado: (%@ segundos) no es una de las opciones optimizadas." + } + } + } +>>>>>>> Stashed changes }, "🦕 End of life Version 🦖 ☄️" : { "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "🦕 Versión de fin de vida 🦖 ☄️" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -1520,6 +1744,12 @@ }, "0" : { "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "0" + } + }, "ja" : { "stringUnit" : { "state" : "translated", @@ -1531,6 +1761,12 @@ }, "1" : { "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "1" + } + }, "ja" : { "stringUnit" : { "state" : "translated", @@ -1542,6 +1778,12 @@ }, "1 byte" : { "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "1 byte" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -1576,6 +1818,12 @@ }, "1 hop away" : { "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "1 salto de distancia" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -1610,6 +1858,12 @@ }, "2.4 Ghz" : { "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "2,4 GHz" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -1638,6 +1892,12 @@ }, "7" : { "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "7" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -1672,6 +1932,12 @@ }, "12 Hour Clock" : { "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Reloj de 12 horas" + } + }, "ja" : { "stringUnit" : { "state" : "translated", @@ -1688,6 +1954,12 @@ }, "25" : { "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "25" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -1722,6 +1994,12 @@ }, "50" : { "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "50" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -1756,6 +2034,12 @@ }, "75" : { "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "75" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -1790,6 +2074,12 @@ }, "100" : { "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "100" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -1824,6 +2114,12 @@ }, "128 bit" : { "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "128 bits" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -1858,6 +2154,12 @@ }, "180" : { "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "180" + } + }, "ja" : { "stringUnit" : { "state" : "translated", @@ -1869,6 +2171,12 @@ }, "256 bit" : { "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "256 bits" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -1903,6 +2211,12 @@ }, "A channel index of 0 indicates the primary channel where broadcast packets are sent from. Location data is broadcast from the first channel where it is enabled with firmware 2.7 forward." : { "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Un índice de canal de 0 indica el canal principal desde donde se envían los paquetes de transmisión. Los datos de ubicación se transmiten desde el primer canal donde está habilitado con el firmware 2.7 en adelante." + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -1913,6 +2227,12 @@ }, "A green lock means the channel is securely encrypted with either a 128 or 256 bit AES key." : { "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Un candado verde significa que el canal está cifrado de forma segura con una clave AES de 128 o 256 bits." + } + }, "ja" : { "stringUnit" : { "state" : "translated", @@ -1935,6 +2255,12 @@ "value" : "In a Meshtastic LoRa Mesh there are up to 8 channels. The first one is the Primary channel where most activity happens and is required. If you don't share your primary channel your first shared channel becomes the primary channel on the other network. It talks on its primary and your secondary channel. A channel with the name 'admin' controls nodes remotely. Other channels are for private groups, each with its own key." } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Un código QR Meshtastic contiene la configuración de LoRa y los valores de canal necesarios para que las radios se comuniquen. Puede compartir una configuración de canal completa usando la opción Reemplazar canales; si elige Agregar canales, sus canales compartidos se agregarán a los canales de la radio receptora." + } + }, "fr" : { "stringUnit" : { "state" : "translated", @@ -1993,6 +2319,12 @@ }, "A red open lock means the channel is not securely encrypted and is used for precise location data, it uses either no key at all or a 1 byte known key." : { "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Un candado rojo abierto significa que el canal no está cifrado de forma segura y se utiliza para datos de ubicación precisos; no utiliza ninguna clave o utiliza una clave conocida de 1 byte." + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -2003,6 +2335,12 @@ }, "A red open lock with a warning means the channel is not securely encrypted and is used for precise location data which is being uplinked to the internet via MQTT, it uses either no key at all or a 1 byte known key." : { "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Un candado rojo abierto con una advertencia significa que el canal no está cifrado de forma segura y se utiliza para datos de ubicación precisos que se conectan a Internet a través de MQTT; no utiliza ninguna clave o utiliza una clave conocida de 1 byte." + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -2013,6 +2351,12 @@ }, "A Trace Route was sent, no response has been received." : { "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Se envió una ruta de rastreo y no se recibió respuesta." + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -2042,6 +2386,12 @@ "A yellow open lock lock means the channel is not securely encrypted but it not used for precise location data, it uses either no key at all or a 1 byte known key." : { "extractionState" : "stale", "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Un candado abierto amarillo significa que el canal no está cifrado de forma segura pero no se utiliza para datos de ubicación precisos; no utiliza ninguna clave o utiliza una clave conocida de 1 byte." + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -2052,7 +2402,15 @@ }, "A yellow open lock means the channel is not securely encrypted but it is not used for precise location data, it uses either no key at all or a 1 byte known key." : { "comment" : "A description of a yellow open lock in the Channels Help view.", - "isCommentAutoGenerated" : true + "isCommentAutoGenerated" : true, + "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Un candado abierto amarillo significa que el canal no está cifrado de forma segura, pero no se utiliza para datos de ubicación precisos; no utiliza ninguna clave o utiliza una clave conocida de 1 byte." + } + } + } }, "About" : { "localizations" : { @@ -2062,6 +2420,12 @@ "value" : "Über Meshtastic" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Acerca de" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -2096,6 +2460,12 @@ "value" : "Über Meshtastic" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Acerca de Meshtastic" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -2136,6 +2506,12 @@ "value" : "Genauigkeit %@" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Exactitud %@" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -2164,6 +2540,12 @@ }, "Ack SNR: %@ dB" : { "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Confirmación SNR: %@ dB" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -2192,6 +2574,12 @@ }, "Ack Time: %@" : { "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Tiempo de confirmación: %@" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -2226,6 +2614,12 @@ "value" : "Bestätigt" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Admitido" + } + }, "fr" : { "stringUnit" : { "state" : "translated", @@ -2284,6 +2678,12 @@ }, "Acknowledged by another node" : { "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Reconocido por otro nodo" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -2318,6 +2718,12 @@ "value" : "Aktionen" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Comportamiento" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -2352,6 +2758,12 @@ "value" : "Aktiv" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Activo" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -2386,6 +2798,12 @@ "value" : "Aktivität" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Actividad" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -2426,6 +2844,12 @@ "value" : "ADC Override" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Anulación del ADC" + } + }, "he" : { "stringUnit" : { "state" : "translated", @@ -2472,6 +2896,12 @@ }, "Add Channel" : { "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Agregar canal" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -2500,6 +2930,12 @@ }, "Add Channels" : { "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Agregar canales" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -2528,6 +2964,12 @@ }, "Add Contact" : { "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Agregar contacto" + } + }, "ja" : { "stringUnit" : { "state" : "translated", @@ -2550,6 +2992,12 @@ }, "Add Meshtastic Node %@ as a contact" : { "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Agregar Meshtastic Node %@ como contacto" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -2584,6 +3032,12 @@ "value" : "Zu Favoriten hinzufügen" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Añadir a favoritos" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -2612,6 +3066,12 @@ }, "Additional help" : { "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ayuda adicional" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -2646,6 +3106,12 @@ }, "Address" : { "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Dirección" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -2680,6 +3146,12 @@ }, "Admin Keys" : { "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Claves de administrador" + } + }, "ja" : { "stringUnit" : { "state" : "translated", @@ -2696,6 +3168,12 @@ }, "Administration" : { "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Administración" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -2730,6 +3208,12 @@ }, "Administration Enabled" : { "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Administración habilitada" + } + }, "ja" : { "stringUnit" : { "state" : "translated", @@ -2752,6 +3236,12 @@ }, "Advanced" : { "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Avanzado" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -2786,6 +3276,12 @@ }, "Advanced Device GPS" : { "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Dispositivo GPS avanzado" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -2821,6 +3317,12 @@ "Advanced GPIO Options" : { "extractionState" : "stale", "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Opciones GPIO avanzadas" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -2855,6 +3357,12 @@ }, "Advanced Position Flags" : { "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Banderas de posición avanzadas" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -2895,6 +3403,12 @@ "value" : "Nach" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Después" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -2965,6 +3479,12 @@ } } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Después de %lld días" + } + }, "ja" : { "variations" : { "plural" : { @@ -3011,6 +3531,12 @@ "value" : "Nach dem Ändern der Einstellungen wird das Gerät neu starten." } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Después de guardar los valores de configuración, el nodo se reiniciará." + } + }, "fr" : { "stringUnit" : { "state" : "translated", @@ -3075,6 +3601,12 @@ "value" : "Nachmittag" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Tarde" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -3109,6 +3641,12 @@ "value" : "Airtime" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Tiempo en antena" + } + }, "fr" : { "stringUnit" : { "state" : "translated", @@ -3167,6 +3705,12 @@ }, "Alert" : { "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Alerta" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -3195,6 +3739,12 @@ }, "Alert GPIO buzzer when receiving a bell" : { "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Alerta al zumbador GPIO al recibir una campana" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -3229,6 +3779,12 @@ }, "Alert GPIO buzzer when receiving a message" : { "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Alerta al zumbador GPIO al recibir un mensaje" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -3257,6 +3813,12 @@ }, "Alert GPIO vibra motor when receiving a bell" : { "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Alertar al motor de vibración GPIO al recibir una campana" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -3291,6 +3853,12 @@ }, "Alert GPIO vibra motor when receiving a message" : { "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Alertar al motor de vibración GPIO al recibir un mensaje" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -3319,6 +3887,12 @@ }, "Alert when receiving a bell" : { "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Alerta al recibir una campana" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -3353,6 +3927,12 @@ }, "Alert when receiving a message" : { "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Alerta al recibir un mensaje" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -3387,6 +3967,12 @@ "value" : "Alle" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Todo" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -3421,6 +4007,12 @@ }, "Allow Position Requests" : { "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Permitir solicitudes de posición" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -3449,6 +4041,12 @@ }, "Alt" : { "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Alt." + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -3483,6 +4081,12 @@ "value" : "Höhe" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Altitud" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -3517,6 +4121,12 @@ "value" : "Höhe %@" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Altitud %@" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -3545,6 +4155,12 @@ }, "Altitude Geoidal Separation" : { "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Separación geoideal de altitud" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -3573,6 +4189,12 @@ }, "Altitude is Mean Sea Level" : { "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "La altitud es el nivel medio del mar." + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -3607,6 +4229,12 @@ "value" : "Immer an" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Siempre encendido" + } + }, "fr" : { "stringUnit" : { "state" : "translated", @@ -3671,6 +4299,12 @@ "value" : "Immer nach Norden zeigen" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Apunta siempre al norte" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -3705,6 +4339,12 @@ "value" : "Ambientebeleuchtung" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Iluminación ambiental" + } + }, "fr" : { "stringUnit" : { "state" : "translated", @@ -3763,6 +4403,12 @@ "value" : "Ambientebeleuchtungskonfiguration" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Configuración de iluminación ambiental" + } + }, "fr" : { "stringUnit" : { "state" : "translated", @@ -3815,6 +4461,12 @@ }, "Ambient Lighting module config received: %@" : { "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Configuración del módulo de iluminación ambiental recibida: %@" + } + }, "fr" : { "stringUnit" : { "state" : "translated", @@ -3879,6 +4531,12 @@ "value" : "Ein quelloffenes, netzunabhängiges, dezentrales Mesh-Netzwerk, das auf kostengünstigen, stromsparenden Funkgeräten läuft." } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Una red de malla descentralizada, fuera de la red y de código abierto que funciona con radios asequibles y de bajo consumo." + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -3911,8 +4569,29 @@ } } }, +<<<<<<< Updated upstream +======= + "Anonymous Usage and Crash data" : { + "comment" : "A description of how the app collects and uses data about its usage and crashes. It emphasizes that this data is anonymous and non-personally identifiable.", + "isCommentAutoGenerated" : true, + "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Datos anónimos de uso y fallos" + } + } + } + }, +>>>>>>> Stashed changes "Any missed messages will be delivered again." : { "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Cualquier mensaje perdido se entregará nuevamente." + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -3953,6 +4632,12 @@ "value" : "Client (Standard) - Mit App verbundener Client." } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Aplicación conectada o dispositivo de mensajería independiente." + } + }, "fr" : { "stringUnit" : { "state" : "translated", @@ -4017,6 +4702,12 @@ "value" : "App-Daten" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Datos de la aplicación" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -4051,6 +4742,12 @@ }, "App Files" : { "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Archivos de aplicación" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -4085,6 +4782,12 @@ }, "App Icon" : { "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Icono de aplicación" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -4101,6 +4804,12 @@ "value" : "Mitteilungseinstellungen" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Notificaciones de aplicaciones" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -4117,6 +4826,12 @@ "value" : "App-Einstellungen" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Configuración de la aplicación" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -4151,6 +4866,12 @@ }, "Apple Apps" : { "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Aplicaciones de Apple" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -4191,6 +4912,12 @@ "value" : "Ungefährer Standort" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ubicación aproximada" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -4219,6 +4946,12 @@ }, "Are you sure you want to delete this message?" : { "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "¿Estás seguro de que deseas eliminar este mensaje?" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -4259,6 +4992,12 @@ "value" : "Bist du sicher dass du den Knoten auf die Werkseinstellungen zurücksetzen willst?" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "¿Está seguro de que desea restablecer el nodo a los valores de fábrica?" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -4299,6 +5038,12 @@ "value" : "Bist Du sicher?" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "¿Está seguro?" + } + }, "fr" : { "stringUnit" : { "state" : "translated", @@ -4357,6 +5102,12 @@ }, "Australia / New Zealand" : { "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Australia / Nueva Zelanda" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -4385,6 +5136,12 @@ }, "Automatically Connect" : { "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Conectar automáticamente" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -4395,6 +5152,12 @@ }, "Automatically toggles to the next page on the screen like a carousel, based the specified interval." : { "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Cambia automáticamente a la página siguiente en la pantalla como un carrusel, según el intervalo especificado." + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -4435,6 +5198,12 @@ "value" : "Verfügbare Modem-Voreinstellungen, Standard ist „Long Range - Fast“." } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Presets de módem disponibles, el valor predeterminado es Long Fast." + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -4475,6 +5244,12 @@ "value" : "Geräte in der Nähe" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Radios disponibles" + } + }, "fr" : { "stringUnit" : { "state" : "translated", @@ -4539,6 +5314,12 @@ "value" : "Zurück" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Atrás" + } + }, "fr" : { "stringUnit" : { "state" : "translated", @@ -4597,6 +5378,12 @@ }, "Backup" : { "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Respaldo" + } + }, "ja" : { "stringUnit" : { "state" : "translated", @@ -4613,6 +5400,12 @@ }, "Backup your private key to your iCloud keychain." : { "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Haga una copia de seguridad de su clave privada en su llavero de iCloud." + } + }, "ja" : { "stringUnit" : { "state" : "translated", @@ -4629,6 +5422,12 @@ }, "Bad" : { "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Malo" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -4657,6 +5456,12 @@ }, "Bad Request" : { "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Solicitud incorrecta" + } + }, "fr" : { "stringUnit" : { "state" : "translated", @@ -4721,6 +5526,12 @@ "value" : "Bandbreite" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ancho de banda" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -4755,6 +5566,12 @@ }, "Bar" : { "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Bar" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -4789,6 +5606,12 @@ }, "Bar Series" : { "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Serie de barras" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -4823,6 +5646,12 @@ }, "Barometric Pressure" : { "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Presión barométrica" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -4863,6 +5692,12 @@ "value" : "Batterie" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Batería" + } + }, "he" : { "stringUnit" : { "state" : "translated", @@ -4922,6 +5757,12 @@ "value" : "Batterie Ladung" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Nivel de batería" + } + }, "fr" : { "stringUnit" : { "state" : "translated", @@ -4987,6 +5828,12 @@ "value" : "Batterie Ladung %" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "% del nivel de batería" + } + }, "fr" : { "stringUnit" : { "state" : "translated", @@ -5052,6 +5899,12 @@ "value" : "Batterie Ladung %d" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Nivel de batería %d" + } + }, "fr" : { "stringUnit" : { "state" : "translated", @@ -5110,6 +5963,12 @@ }, "Baud" : { "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "baudios" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -5156,6 +6015,12 @@ "value" : "Biken" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ciclismo" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -5190,6 +6055,12 @@ }, "BLE" : { "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "BLE" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -5230,6 +6101,12 @@ "value" : "Die Bluetooth Pin muss 6 Stellen lang sein." } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "El pin BLE debe tener 6 dígitos." + } + }, "fr" : { "stringUnit" : { "state" : "translated", @@ -5294,6 +6171,12 @@ "value" : "Bluetooth" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "bluetooth" + } + }, "fr" : { "stringUnit" : { "state" : "translated", @@ -5358,6 +6241,12 @@ "value" : "Bluetooth Konfiguration" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Configuración de Bluetooth" + } + }, "fr" : { "stringUnit" : { "state" : "translated", @@ -5422,6 +6311,12 @@ "value" : "Bluetooth Konfiguration empfangen: %@" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Configuración de Bluetooth recibida: %@" + } + }, "fr" : { "stringUnit" : { "state" : "translated", @@ -5482,6 +6377,12 @@ "comment" : "A heading displayed on a view that guides users to configure Bluetooth connectivity for the app.", "isCommentAutoGenerated" : true, "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Conectividad Bluetooth" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -5492,6 +6393,12 @@ }, "Bold Heading" : { "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Encabezado en negrita" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -5502,6 +6409,12 @@ }, "Bold the heading text on the screen." : { "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Negrita el texto del encabezado en la pantalla." + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -5511,10 +6424,23 @@ } }, "Broadcast Device Metrics" : { - + "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Métricas de dispositivos de transmisión" + } + } + } }, "Broadcast Interval" : { "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Intervalo de transmisión" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -5555,6 +6481,12 @@ "value" : "Sendet GPS-Positionspakete mit Priorität." } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Transmite paquetes de posición GPS como prioridad." + } + }, "fr" : { "stringUnit" : { "state" : "translated", @@ -5619,6 +6551,12 @@ "value" : "Sendet den Standort regelmäßig als Nachricht an den Standardkanal, um die Suche nach dem Gerät zu unterstützen." } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Transmite la ubicación como mensaje al canal predeterminado con regularidad para ayudar con la recuperación del dispositivo." + } + }, "fr" : { "stringUnit" : { "state" : "translated", @@ -5683,6 +6621,12 @@ "value" : "Sendet Telemetriepakete mit Priorität." } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Transmite paquetes de telemetría como prioridad." + } + }, "fr" : { "stringUnit" : { "state" : "translated", @@ -5741,6 +6685,12 @@ }, "Button GPIO" : { "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Botón GPIO" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -5775,6 +6725,12 @@ }, "Buy Complete Radios" : { "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Comprar Radios Completas" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -5809,6 +6765,12 @@ }, "Buzzer GPIO" : { "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Zumbador GPIO" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -5843,6 +6805,12 @@ }, "By enabling this feature, you acknowledge and expressly consent to the transmission of your device’s real-time geographic location over the MQTT protocol without encryption. This location data may be used for purposes such as live map reporting, device tracking, and related telemetry functions." : { "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Al habilitar esta función, usted reconoce y acepta expresamente la transmisión de la ubicación geográfica en tiempo real de su dispositivo a través del protocolo MQTT sin cifrado. Estos datos de ubicación se pueden utilizar para fines tales como informes de mapas en vivo, seguimiento de dispositivos y funciones de telemetría relacionadas." + } + }, "ja" : { "stringUnit" : { "state" : "translated", @@ -5871,6 +6839,12 @@ "value" : "Bytes" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "bytes" + } + }, "fr" : { "stringUnit" : { "state" : "translated", @@ -5930,6 +6904,12 @@ "Bytes Used" : { "comment" : "VoiceOver value for bytes used", "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Bytes utilizados" + } + }, "ja" : { "stringUnit" : { "state" : "translated", @@ -5952,6 +6932,12 @@ "value" : "Rufzeichen" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Señal de llamada" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -5992,6 +6978,12 @@ "value" : "Das Rufzeichen darf nicht leer sein." } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "El indicativo de llamada no debe estar vacío" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -6032,6 +7024,12 @@ "value" : "Abbrechen" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Cancelar" + } + }, "fr" : { "stringUnit" : { "state" : "translated", @@ -6090,6 +7088,12 @@ }, "Canned Message module config received: %@" : { "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Configuración del módulo de mensajes predefinidos recibida: %@" + } + }, "fr" : { "stringUnit" : { "state" : "translated", @@ -6154,6 +7158,12 @@ "value" : "Canned Messages" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Mensajes enlatados" + } + }, "fr" : { "stringUnit" : { "state" : "translated", @@ -6218,6 +7228,12 @@ "value" : "Canned Messages Config" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Configuración de mensajes predefinidos" + } + }, "fr" : { "stringUnit" : { "state" : "translated", @@ -6276,6 +7292,12 @@ }, "Canned Messages Messages Received For: %@" : { "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Mensajes almacenados Mensajes recibidos para: %@" + } + }, "fr" : { "stringUnit" : { "state" : "translated", @@ -6334,6 +7356,12 @@ }, "Carousel Interval" : { "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Intervalo de carrusel" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -6368,6 +7396,12 @@ "value" : "Kategorien" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Categorías" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -6408,6 +7442,12 @@ "value" : "Kategorie" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Categoría" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -6436,6 +7476,12 @@ }, "Ch1 Current" : { "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Corriente Ch1" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -6464,6 +7510,12 @@ }, "Ch1 Voltage" : { "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Voltaje Ch1" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -6492,6 +7544,12 @@ }, "Ch2 Current" : { "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Corriente Ch2" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -6520,6 +7578,12 @@ }, "Ch2 Voltage" : { "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Voltaje Ch2" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -6548,6 +7612,12 @@ }, "Ch3 Current" : { "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ch3 actual" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -6576,6 +7646,12 @@ }, "Ch3 Voltage" : { "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Voltaje Ch3" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -6610,6 +7686,12 @@ "value" : "Kanal" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Canal" + } + }, "fr" : { "stringUnit" : { "state" : "translated", @@ -6668,6 +7750,12 @@ }, "Channel 0 Included" : { "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Canal 0 incluido" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -6702,6 +7790,12 @@ }, "Channel 1" : { "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Canal 1" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -6730,6 +7824,12 @@ }, "Channel 1 Included" : { "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Canal 1 incluido" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -6764,6 +7864,12 @@ }, "Channel 2" : { "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Canal 2" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -6792,6 +7898,12 @@ }, "Channel 2 Included" : { "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Canal 2 incluido" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -6826,6 +7938,12 @@ }, "Channel 3" : { "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Canal 3" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -6854,6 +7972,12 @@ }, "Channel 3 Included" : { "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Canal 3 incluido" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -6888,6 +8012,12 @@ }, "Channel 4 Included" : { "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Canal 4 incluido" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -6922,6 +8052,12 @@ }, "Channel 5 Included" : { "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Canal 5 incluido" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -6956,6 +8092,12 @@ }, "Channel 6 Included" : { "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Canal 6 incluido" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -6990,6 +8132,12 @@ }, "Channel 7 Included" : { "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Canal 7 incluido" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -7024,6 +8172,12 @@ }, "Channel Details" : { "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Detalles del canal" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -7058,6 +8212,12 @@ }, "Channel Name" : { "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Nombre del canal" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -7092,6 +8252,12 @@ }, "Channel number must be between 0 and 7." : { "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "El número de canal debe estar entre 0 y 7." + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -7126,6 +8292,12 @@ }, "Channel Role" : { "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Rol del canal" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -7160,6 +8332,12 @@ }, "Channel URL" : { "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "URL del canal" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -7194,6 +8372,12 @@ "value" : "Kanalbelegung" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Utilización del canal" + } + }, "fr" : { "stringUnit" : { "state" : "translated", @@ -7252,6 +8436,12 @@ }, "Channel Utilization %@%%" : { "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Utilización del canal %@%%" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -7286,6 +8476,12 @@ "value" : "Kanäle" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Canales" + } + }, "fr" : { "stringUnit" : { "state" : "translated", @@ -7344,6 +8540,12 @@ }, "Channels being added from the QR code did not save. When adding channels the names must be unique." : { "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Los canales que se agregaron desde el código QR no se guardaron. Al agregar canales, los nombres deben ser únicos." + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -7378,6 +8580,12 @@ }, "Channels Help" : { "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ayuda de canales" + } + }, "ja" : { "stringUnit" : { "state" : "translated", @@ -7394,6 +8602,12 @@ }, "Chart" : { "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Cuadro" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -7428,6 +8642,12 @@ }, "CHG" : { "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "CHG" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -7456,6 +8676,12 @@ }, "China" : { "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Porcelana" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -7484,6 +8710,12 @@ }, "Chirpy" : { "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Alegre" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -7494,6 +8726,12 @@ }, "Clear" : { "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Claro" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -7534,6 +8772,12 @@ "value" : "App-Daten löschen" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Borrar datos de la aplicación" + } + }, "fr" : { "stringUnit" : { "state" : "translated", @@ -7592,6 +8836,12 @@ }, "Clear Log" : { "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Borrar registro" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -7632,6 +8882,12 @@ "value" : "Veraltete Knoten löschen" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Borrar nodos obsoletos" + } + }, "ja" : { "stringUnit" : { "state" : "translated", @@ -7648,6 +8904,12 @@ }, "Client" : { "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Cliente" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -7692,6 +8954,12 @@ "value" : "Client - Versteckt" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Cliente oculto" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -7726,6 +8994,12 @@ }, "Client History" : { "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Historial del cliente" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -7760,6 +9034,12 @@ }, "Client History Request Sent" : { "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Solicitud de historial del cliente enviada" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -7794,6 +9074,12 @@ }, "Client Mute" : { "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Silencio del cliente" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -7828,6 +9114,12 @@ }, "Client options" : { "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Opciones del cliente" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -7862,6 +9154,12 @@ }, "Clockwise Rotary Event" : { "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Evento giratorio en el sentido de las agujas del reloj" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -7902,6 +9200,12 @@ "value" : "Schließen" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Cerca" + } + }, "fr" : { "stringUnit" : { "state" : "translated", @@ -7960,6 +9264,12 @@ }, "Coding Rate" : { "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Tasa de codificación" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -8000,6 +9310,12 @@ "value" : "Farbe" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Color" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -8040,6 +9356,12 @@ "value" : "Bleibe mit deinen Freunden und deiner Community in Verbindung, auch abseits vom Mobilfunknetz." } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Comuníquese fuera de la red con sus amigos y su comunidad sin servicio celular." + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -8050,6 +9372,12 @@ }, "Communicating" : { "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Comunicado" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -8078,6 +9406,12 @@ }, "Community Support" : { "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Apoyo comunitario" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -8109,6 +9443,12 @@ }, "Config" : { "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "configuración" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -8149,6 +9489,12 @@ "value" : "Konfiguration für: %@" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Configuración para: %@" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -8177,6 +9523,12 @@ }, "Configuration Presets" : { "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Preajustes de configuración" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -8217,6 +9569,12 @@ "value" : "Konfigurieren" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Configurar" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -8247,6 +9605,12 @@ "comment" : "Button label to guide users to configure Bluetooth connectivity for the app.", "isCommentAutoGenerated" : true, "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Configurar la conectividad Bluetooth" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -8259,6 +9623,12 @@ "comment" : "Button label to configure local network access permissions.", "isCommentAutoGenerated" : true, "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Configurar el acceso a la red local" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -8275,6 +9645,12 @@ "value" : "Standortberechtigungen konfigurieren" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Configurar permisos de ubicación" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -8291,6 +9667,12 @@ "value" : "Mitteilungsberechtigungen konfigurieren" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Configurar permisos de notificación" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -8301,6 +9683,12 @@ }, "Confirm" : { "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Confirmar" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -8329,6 +9717,12 @@ }, "Connect" : { "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Conectar" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -8351,6 +9745,12 @@ "value" : "Verbunden mit einem Knoten" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Conectarse a un nodo" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -8379,6 +9779,12 @@ }, "Connect to MQTT via Proxy" : { "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Conéctese a MQTT a través de Proxy" + } + }, "ja" : { "stringUnit" : { "state" : "translated", @@ -8401,6 +9807,12 @@ }, "Connect to new radio?" : { "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "¿Conectar a una nueva radio?" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -8435,6 +9847,12 @@ "value" : "Derzeit verbunden" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Conectado" + } + }, "fr" : { "stringUnit" : { "state" : "translated", @@ -8499,6 +9917,12 @@ "value" : "Verbunden mit Knoten %@" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Nodo conectado %@" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -8527,6 +9951,12 @@ }, "Connected Radio" : { "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Radio conectada" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -8561,6 +9991,12 @@ "value" : "Verbinde..." } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Conectando. ." + } + }, "fr" : { "stringUnit" : { "state" : "translated", @@ -8619,6 +10055,12 @@ }, "Connecting to a new radio will clear all app data on the phone." : { "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Al conectarse a una nueva radio se borrarán todos los datos de la aplicación en el teléfono." + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -8653,6 +10095,12 @@ "value" : "Verbindungsversuch %lld von 10" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Intento de conexión %lld de 10" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -8687,6 +10135,12 @@ }, "Connection Name" : { "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Nombre de conexión" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -8697,6 +10151,12 @@ }, "Consent to Share Unencrypted Node Data via MQTT" : { "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Consentimiento para compartir datos de nodos no cifrados a través de MQTT" + } + }, "ja" : { "stringUnit" : { "state" : "translated", @@ -8725,6 +10185,12 @@ "value" : "Kontaktfilter" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Filtros de contacto" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -8735,6 +10201,12 @@ }, "Contact URL" : { "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "URL de contacto" + } + }, "ja" : { "stringUnit" : { "state" : "translated", @@ -8763,6 +10235,12 @@ "value" : "Kontakte (%@)" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Contactos (%@)" + } + }, "fr" : { "stringUnit" : { "state" : "translated", @@ -8821,6 +10299,12 @@ }, "Control Type" : { "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Tipo de control" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -8855,6 +10339,12 @@ }, "Controls the blinking LED on the device. For most devices this will control one of the up to 4 LEDS, the charger and GPS LEDs are not controllable." : { "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Controla el LED parpadeante en el dispositivo. Para la mayoría de los dispositivos, esto controlará uno de los hasta 4 LED, los LED del cargador y del GPS no son controlables." + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -8895,6 +10385,12 @@ "value" : "Konvexe Hülle" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Casco convexo" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -8929,6 +10425,12 @@ "value" : "Koordinate" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Coordinar" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -8963,6 +10465,12 @@ "value" : "Koordinate %1$@, %2$@" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Coordinar %@, %@" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -9003,6 +10511,12 @@ "value" : "Koordinaten:" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Coordenadas:" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -9037,6 +10551,12 @@ "value" : "Kopieren" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Copiar" + } + }, "fr" : { "stringUnit" : { "state" : "translated", @@ -9101,6 +10621,12 @@ "value" : "Knoten nicht gefunden" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "No se pudo encontrar el nodo" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -9135,6 +10661,12 @@ }, "Counter Clockwise Rotary Event" : { "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Evento giratorio en sentido antihorario" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -9169,6 +10701,12 @@ "value" : "Wegpunkt erstellen" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Crear punto de referencia" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -9203,6 +10741,12 @@ "value" : "Erstelle deine eigenen Netzwerke" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Crea tus propias redes" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -9219,6 +10763,12 @@ "value" : "Erstellt: %@" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Creado: %@" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -9253,6 +10803,12 @@ "value" : "Kritische Hinweise" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Alertas críticas" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -9263,6 +10819,12 @@ }, "Current" : { "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Actual" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -9297,6 +10859,12 @@ "value" : "Aktuelle Firmware Version: %@" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Versión de firmware actual: %@" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -9337,6 +10905,12 @@ "value" : "Aktuelle Firmware Version: %1$@, neuste Firmware Version %2$@" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Versión de firmware actual: %@, Última versión de firmware: %@" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -9377,6 +10951,12 @@ "value" : "Aktuell: %lld" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Actual: %lld" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -9405,6 +10985,12 @@ }, "Currently the recommended way to update ESP32 devices is using the web flasher on a desktop computer from a chrome based browser. It does not work on mobile devices or over BLE." : { "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Actualmente, la forma recomendada de actualizar dispositivos ESP32 es utilizar el flash web en una computadora de escritorio desde un navegador basado en Chrome. No funciona en dispositivos móviles ni a través de BLE." + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -9445,6 +11031,12 @@ "value" : "Datum" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Fecha" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -9473,6 +11065,12 @@ }, "Debug" : { "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Depurar" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -9513,6 +11111,12 @@ "value" : "Fehlersuchprotokolle" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Registros de depuración" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -9541,6 +11145,12 @@ }, "Debug Logs%@" : { "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Registros de depuración%@" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -9575,6 +11185,12 @@ "value" : "Standard" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Por defecto" + } + }, "fr" : { "stringUnit" : { "state" : "translated", @@ -9633,6 +11249,12 @@ }, "Default 128x64 screen layout" : { "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Diseño de pantalla predeterminado de 128x64" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -9673,6 +11295,12 @@ "value" : "Löschen" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Borrar" + } + }, "fr" : { "stringUnit" : { "state" : "translated", @@ -9731,6 +11359,12 @@ }, "Delete all config, keys and BLE bonds? " : { "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "¿Eliminar todas las configuraciones, claves y enlaces BLE?" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -9753,6 +11387,12 @@ }, "Delete all config? " : { "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "¿Eliminar todas las configuraciones?" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -9775,6 +11415,12 @@ }, "Delete all device metrics?" : { "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "¿Eliminar todas las métricas del dispositivo?" + } + }, "fr" : { "stringUnit" : { "state" : "translated", @@ -9833,6 +11479,12 @@ }, "Delete all environment metrics?" : { "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "¿Eliminar todas las métricas del entorno?" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -9867,6 +11519,12 @@ }, "Delete all pax data?" : { "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "¿Eliminar todos los datos de los pasajeros?" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -9901,6 +11559,12 @@ }, "Delete all positions?" : { "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "¿Eliminar todas las posiciones?" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -9929,6 +11593,12 @@ }, "Delete Message" : { "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Eliminar mensaje" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -9957,6 +11627,12 @@ }, "Delete Messages" : { "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Eliminar mensajes" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -9991,6 +11667,12 @@ "value" : "Knoten löschen" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Eliminar nodo" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -10025,6 +11707,12 @@ "value" : "Knoten löschen?" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "¿Eliminar nodo?" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -10053,6 +11741,12 @@ }, "Delete Power metrics?" : { "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "¿Eliminar métricas de potencia?" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -10087,6 +11781,12 @@ "value" : "Beschreibung" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Descripción" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -10115,6 +11815,12 @@ }, "Description must be less than 100 bytes" : { "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "La descripción debe tener menos de 100 bytes." + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -10149,6 +11855,12 @@ }, "Details..." : { "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Detalles..." + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -10159,6 +11871,12 @@ }, "Detection" : { "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Detección" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -10187,6 +11905,12 @@ }, "Detection event" : { "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Evento de detección" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -10222,6 +11946,12 @@ "value" : "Detection Sensor" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Sensor de detección" + } + }, "fr" : { "stringUnit" : { "state" : "translated", @@ -10280,6 +12010,12 @@ }, "Detection Sensor Config" : { "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Configuración del sensor de detección" + } + }, "fr" : { "stringUnit" : { "state" : "translated", @@ -10332,6 +12068,12 @@ }, "Detection Sensor Log" : { "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Registro del sensor de detección" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -10366,6 +12108,12 @@ }, "Detection sensor messages are received as text messages. If you enable notifications you will recieve a notification for each detection message received and a corresponding unread message badge." : { "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Los mensajes del sensor de detección se reciben como mensajes de texto. Si habilita las notificaciones, recibirá una notificación por cada mensaje de detección recibido y la correspondiente insignia de mensaje no leído." + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -10400,6 +12148,12 @@ }, "Detection Sensor module config received: %@" : { "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Configuración del módulo del sensor de detección recibida: %@" + } + }, "fr" : { "stringUnit" : { "state" : "translated", @@ -10458,6 +12212,12 @@ }, "Developers" : { "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Desarrolladores" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -10498,6 +12258,12 @@ "value" : "Gerät" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Dispositivo" + } + }, "fr" : { "stringUnit" : { "state" : "translated", @@ -10562,6 +12328,12 @@ "value" : "Gerätekonfiguration" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Configuración del dispositivo" + } + }, "fr" : { "stringUnit" : { "state" : "translated", @@ -10626,6 +12398,12 @@ "value" : "Gerätekonfiguration empfangen: %@" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Configuración del dispositivo recibida: %@" + } + }, "fr" : { "stringUnit" : { "state" : "translated", @@ -10690,6 +12468,12 @@ "value" : "Gerätekonfiguration" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Configuración del dispositivo" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -10736,6 +12520,12 @@ "value" : "Geräte-GPS" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Dispositivo GPS" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -10770,6 +12560,12 @@ }, "Device is managed by a mesh administrator, the user is unable to access any of the device settings." : { "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "El dispositivo es administrado por un administrador de malla, el usuario no puede acceder a ninguna de las configuraciones del dispositivo." + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -10810,6 +12606,12 @@ "value" : "Device Metadata empfangen von: %@" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Metadatos del dispositivo recibidos de: %@" + } + }, "fr" : { "stringUnit" : { "state" : "translated", @@ -10868,6 +12670,12 @@ }, "Device Metrics" : { "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Métricas del dispositivo" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -10902,6 +12710,12 @@ }, "Device Metrics Log" : { "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Registro de métricas del dispositivo" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -10942,6 +12756,12 @@ "value" : "Gerätemodell: %@" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Modelo de dispositivo: %@" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -10975,10 +12795,23 @@ } }, "Device Options" : { - + "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Opciones del dispositivo" + } + } + } }, "Device Role" : { "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Función del dispositivo" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -11013,6 +12846,12 @@ }, "Device Screen" : { "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Pantalla del dispositivo" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -11053,6 +12892,12 @@ "value" : "Gerät, das keine Pakete von anderen Geräten weiterleitet." } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Dispositivo que no reenvía paquetes desde otros dispositivos." + } + }, "fr" : { "stringUnit" : { "state" : "translated", @@ -11117,6 +12962,12 @@ "value" : "Gerät, das nur bei Bedarf sendet, um nicht entdeckt zu werden oder Strom zu sparen." } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Dispositivo que solo transmite según sea necesario para sigilo o ahorro de energía." + } + }, "fr" : { "stringUnit" : { "state" : "translated", @@ -11175,6 +13026,12 @@ }, "Dilution of precision (DOP) PDOP used by default" : { "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Dilución de precisión (DOP) PDOP utilizado por defecto" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -11209,6 +13066,12 @@ "value" : "Direkt" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Directo" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -11249,6 +13112,12 @@ "value" : "Hilfe für Direktnachrichten" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ayuda por mensaje directo" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -11283,6 +13152,12 @@ }, "Direct Message Key" : { "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Tecla de mensaje directo" + } + }, "ja" : { "stringUnit" : { "state" : "translated", @@ -11305,6 +13180,12 @@ "value" : "Direktnachrichten" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Mensajes directos" + } + }, "fr" : { "stringUnit" : { "state" : "translated", @@ -11363,6 +13244,12 @@ }, "Direct messages are using the new public key infrastructure for encryption. Requires firmware version 2.5 or greater." : { "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Los mensajes directos utilizan la nueva infraestructura de clave pública para el cifrado. Requiere versión de firmware 2.5 o superior." + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -11397,6 +13284,12 @@ }, "Direct messages are using the shared key for the channel." : { "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Los mensajes directos utilizan la clave compartida del canal." + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -11437,6 +13330,12 @@ "value" : "Deaktiviert" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Desactivado" + } + }, "fr" : { "stringUnit" : { "state" : "translated", @@ -11501,6 +13400,12 @@ "value" : "Trennen" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Desconectar" + } + }, "fr" : { "stringUnit" : { "state" : "translated", @@ -11559,6 +13464,12 @@ }, "Disconnect Node" : { "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Desconectar nodo" + } + }, "ja" : { "stringUnit" : { "state" : "translated", @@ -11575,6 +13486,12 @@ }, "Disconnect the currently connected node" : { "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Desconectar el nodo actualmente conectado" + } + }, "ja" : { "stringUnit" : { "state" : "translated", @@ -11597,6 +13514,12 @@ "value" : "Tastatur ausblenden" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Despedir" + } + }, "fr" : { "stringUnit" : { "state" : "translated", @@ -11661,6 +13584,12 @@ "value" : "Display (Device Screen)" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Mostrar" + } + }, "fr" : { "stringUnit" : { "state" : "translated", @@ -11719,6 +13648,12 @@ }, "Display Config" : { "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Configuración de pantalla" + } + }, "fr" : { "stringUnit" : { "state" : "translated", @@ -11783,6 +13718,12 @@ "value" : "Display Konfiguration empfangen: %@" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Configuración de pantalla recibida: %@" + } + }, "fr" : { "stringUnit" : { "state" : "translated", @@ -11841,6 +13782,12 @@ }, "Display Fahrenheit" : { "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Mostrar grados Fahrenheit" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -11875,6 +13822,12 @@ }, "Display Mode" : { "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Modo de visualización" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -11915,6 +13868,12 @@ "value" : "Darstellung der Entfernung zwischen deinem Handy und anderen Meshtastic-Knoten mit Positionsangabe." } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Muestra la distancia entre tu teléfono y otros nodos Meshtastic con posiciones." + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -11925,6 +13884,12 @@ }, "Display Units" : { "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Unidades de visualización" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -11965,6 +13930,12 @@ "value" : "Distanz" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Distancia" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -12005,6 +13976,12 @@ "value" : "Distanzfilter" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Filtros de distancia" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -12021,6 +13998,12 @@ "value" : "Distanzmessungen" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Mediciones de distancia" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -12040,6 +14023,12 @@ "value" : "Dokumentation" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Documentación" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -12074,6 +14063,12 @@ }, "Done" : { "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Hecho" + } + }, "ja" : { "stringUnit" : { "state" : "translated", @@ -12096,6 +14091,12 @@ }, "Double Tap as Button" : { "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Toque dos veces como botón" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -12136,6 +14137,12 @@ "value" : "Runter" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Abajo" + } + }, "fr" : { "stringUnit" : { "state" : "translated", @@ -12194,6 +14201,12 @@ }, "Downlink Enabled" : { "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Enlace descendente habilitado" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -12228,6 +14241,12 @@ }, "Drag & Drop Firmware Update" : { "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Actualización de firmware de arrastrar y soltar" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -12262,6 +14281,12 @@ }, "Drag & Drop Firmware Update Documentation" : { "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Documentación de actualización de firmware de arrastrar y soltar" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -12296,6 +14321,12 @@ }, "Drag & Drop is the recommended way to update firmware for NRF devices. If your iPhone or iPad is USB-C it will work with your regular USB-C charging cable, for lightning devices you need the Apple Lightning to USB camera adaptor." : { "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Arrastrar y soltar es la forma recomendada de actualizar el firmware para dispositivos NRF. Si su iPhone o iPad es USB-C, funcionará con su cable de carga USB-C habitual; para dispositivos Lightning, necesita el adaptador de cámara Lightning a USB de Apple." + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -12336,6 +14367,12 @@ "value" : "Fahren" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Conduciendo" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -12370,6 +14407,12 @@ }, "Drop Pin in Maps" : { "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Colocar pin en mapas" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -12404,6 +14447,12 @@ "value" : "Richte einfach private Mesh-Netzwerke für eine sichere und zuverlässige Kommunikation in abgelegenen Gebieten ein." } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Configure fácilmente redes de malla privadas para una comunicación segura y confiable en áreas remotas." + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -12414,6 +14463,12 @@ }, "Echo" : { "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Eco" + } + }, "fr" : { "stringUnit" : { "state" : "translated", @@ -12466,6 +14521,12 @@ }, "Editing Waypoint" : { "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Editar punto de referencia" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -12500,6 +14561,12 @@ "value" : "Achtzehn Stunden" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Dieciocho horas" + } + }, "fr" : { "stringUnit" : { "state" : "translated", @@ -12564,6 +14631,12 @@ "value" : "Höhenunterschied" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Elev. Ganar" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -12592,6 +14665,12 @@ }, "Emoji" : { "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "emojis" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -12626,6 +14705,12 @@ }, "Empty" : { "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Vacío" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -12659,10 +14744,23 @@ } }, "Enable broadcasting device metrics to the mesh network. When disabled, metrics are only sent to connected clients." : { - + "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Habilite la transmisión de métricas de dispositivos a la red de malla. Cuando está deshabilitado, las métricas solo se envían a los clientes conectados." + } + } + } }, "Enable broadcasting packets via UDP over the local network." : { "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Habilite la transmisión de paquetes a través de UDP a través de la red local." + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -12697,6 +14795,12 @@ "value" : "Standortfreigabe aktivieren" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Habilitar compartir ubicación" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -12707,6 +14811,12 @@ }, "Enable Notifications" : { "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Habilitar notificaciones" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -12741,6 +14851,12 @@ }, "Enable this device as a Store and Forward server. Requires an ESP32 device with PSRAM." : { "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Habilite este dispositivo como servidor Store and Forward. Requiere un dispositivo ESP32 con PSRAM." + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -12775,6 +14891,12 @@ "value" : "Aktiviert" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Activado" + } + }, "fr" : { "stringUnit" : { "state" : "translated", @@ -12839,6 +14961,12 @@ "value" : "Aktiviert automatische TAK-PLI-Übertragungen und verringert die Anzahl der Routineübertragungen." } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Permite transmisiones automáticas de TAK PLI y reduce las transmisiones de rutina." + } + }, "fr" : { "stringUnit" : { "state" : "translated", @@ -12897,6 +15025,12 @@ }, "Enables devices with native I2S audio output to use the RTTTL over speaker like a buzzer. T-Watch S3 and T-Deck for example have this capability." : { "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Permite que los dispositivos con salida de audio I2S nativa utilicen el RTTTL a través del altavoz como un timbre. T-Watch S3 y T-Deck, por ejemplo, tienen esta capacidad." + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -12937,6 +15071,12 @@ "value" : "Aktiviert den blauen Standort-Punkt für dein Handy in der Mesh-Karte." } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Habilita el punto de ubicación azul para su teléfono en el mapa de malla." + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -12947,6 +15087,12 @@ }, "Enables the detection sensor module, it needs to be enabled on both the node with the sensor, and any nodes that you want to receive detection sensor text messages or view the detection sensor log and chart." : { "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Habilita el módulo del sensor de detección; debe estar habilitado tanto en el nodo con el sensor como en cualquier nodo en el que desee recibir mensajes de texto del sensor de detección o ver el registro y el gráfico del sensor de detección." + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -12981,6 +15127,12 @@ }, "Enables the store and forward module." : { "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Habilita el módulo de almacenamiento y reenvío." + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -13009,6 +15161,12 @@ }, "Enabling Ethernet will disable the bluetooth connection to the app." : { "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Al habilitar Ethernet se deshabilitará la conexión bluetooth a la aplicación." + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -13031,6 +15189,12 @@ }, "Enabling WiFi will disable the bluetooth connection to the app." : { "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Habilitar WiFi deshabilitará la conexión bluetooth a la aplicación." + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -13041,6 +15205,12 @@ }, "Encoder Press Event" : { "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Evento de prensa del codificador" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -13075,6 +15245,12 @@ "value" : "Verschlüsselt" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "cifrado" + } + }, "fr" : { "stringUnit" : { "state" : "translated", @@ -13140,6 +15316,12 @@ "value" : "Verschlüsseltes Senden fehlgeschlagen" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Error de envío cifrado" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -13168,6 +15350,12 @@ }, "Encryption Enabled" : { "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Cifrado habilitado" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -13208,6 +15396,12 @@ "value" : "DFÜ-Modus aktivieren" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ingrese al modo DFU" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -13244,6 +15438,12 @@ "comment" : "A label for a text field where the user can enter a hostname or IP address and optionally a port number.", "isCommentAutoGenerated" : true, "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Introduzca el nombre de host[:puerto]" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -13260,6 +15460,12 @@ "value" : "Umgebung" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "ambiente" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -13300,6 +15506,12 @@ "value" : "Umgebung" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ambiente" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -13334,6 +15546,12 @@ }, "Environment Metrics" : { "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Métricas ambientales" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -13367,10 +15585,23 @@ } }, "Environment Metrics Enabled" : { - + "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Métricas de entorno habilitadas" + } + } + } }, "Environment Metrics Log" : { "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Registro de métricas ambientales" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -13404,7 +15635,14 @@ } }, "Environment Sensor Options" : { - + "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Opciones de sensores ambientales" + } + } + } }, "Erase all app data?" : { "localizations" : { @@ -13414,6 +15652,12 @@ "value" : "Alle App-Daten löschen?" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "¿Borrar todos los datos de la aplicación?" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -13454,6 +15698,12 @@ "value" : "Alle Geräte- und App-Daten löschen?" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "¿Borrar todos los datos del dispositivo y de las aplicaciones?" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -13488,6 +15738,12 @@ }, "Error: %@" : { "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Error: %@" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -13522,6 +15778,12 @@ }, "ESP 32 OTA update is a work in progress, click the button below to send your device a reboot into ota admin message." : { "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "La actualización de ESP 32 OTA es un trabajo en progreso, haga clic en el botón a continuación para enviar su dispositivo a un reinicio en el mensaje de administrador de ota." + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -13556,6 +15818,12 @@ }, "ESP32 Device Firmware Update" : { "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Actualización del firmware del dispositivo ESP32" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -13590,6 +15858,12 @@ }, "Ethernet Options" : { "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Opciones de Ethernet" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -13618,6 +15892,12 @@ }, "European Union 433MHz" : { "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Unión Europea 433MHz" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -13646,6 +15926,12 @@ }, "European Union 868MHz" : { "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Unión Europea 868MHz" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -13680,6 +15966,12 @@ "value" : "Abend" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Noche" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -13708,6 +16000,12 @@ }, "Exchange Positions" : { "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Posiciones de intercambio" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -13742,6 +16040,12 @@ "value" : "Ausrufezeichen" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Exclamación" + } + }, "fr" : { "stringUnit" : { "state" : "translated", @@ -13800,6 +16104,12 @@ }, "Expiration" : { "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Vencimiento" + } + }, "ja" : { "stringUnit" : { "state" : "translated", @@ -13822,6 +16132,12 @@ "value" : "Zeitpunkt" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Expirar" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -13856,6 +16172,12 @@ "value" : "Automatisches Löschen" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Vence" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -13884,6 +16206,12 @@ }, "Expires: %@" : { "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Vence: %@" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -13918,6 +16246,12 @@ "value" : "Exportieren" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Exportar" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -13952,6 +16286,12 @@ "value" : "Externe Benachrichtigung" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Notificación externa" + } + }, "fr" : { "stringUnit" : { "state" : "translated", @@ -14016,6 +16356,12 @@ "value" : "Einstellungen der externen Benachrichtigung" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Configuración de notificación externa" + } + }, "fr" : { "stringUnit" : { "state" : "translated", @@ -14074,6 +16420,12 @@ }, "External Notification module config received: %@" : { "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Configuración del módulo de notificación externa recibida: %@" + } + }, "fr" : { "stringUnit" : { "state" : "translated", @@ -14138,6 +16490,12 @@ "value" : "Werkseinstellungen" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Restablecimiento de fábrica" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -14166,6 +16524,12 @@ }, "Factory reset will delete device and app data." : { "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "El restablecimiento de fábrica eliminará los datos del dispositivo y de la aplicación." + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -14188,6 +16552,12 @@ }, "Failed to encode message content" : { "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "No se pudo codificar el contenido del mensaje" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -14216,6 +16586,12 @@ }, "Failed to get a valid position to exchange" : { "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "No se pudo obtener una posición válida para intercambiar" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -14244,6 +16620,12 @@ }, "Failed to get a valid position to exchange." : { "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "No se pudo obtener una posición válida para intercambiar." + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -14278,6 +16660,12 @@ "value" : "Ordentliche Signalstärke" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Justo" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -14312,6 +16700,12 @@ "value" : "Favorit" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Favorito" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -14346,6 +16740,12 @@ "value" : "Knoten, die als Favorit markiert oder ignoriert wurden, bleiben immer erhalten. Knoten ohne PKC-Schlüssel werden gemäß dem festgelegten Zeitplan aus der App-Datenbank gelöscht. Knoten mit PKC-Schlüsseln werden nur gelöscht, wenn das Intervall auf 7 Tage oder länger eingestellt ist. Diese Funktion löscht nur Knoten aus der App, die nicht in der Geräteknoten-Datenbank gespeichert sind." } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Los nodos favoritos e ignorados siempre se conservan. Los nodos sin claves PKC se borran de la base de datos de la aplicación según el cronograma establecido por el usuario, los nodos con claves PKC se borran solo si el intervalo se establece en 7 días o más. Esta función solo elimina los nodos de la aplicación que no están almacenados en la base de datos de nodos del dispositivo." + } + }, "ja" : { "stringUnit" : { "state" : "translated", @@ -14368,6 +16768,12 @@ "value" : "Favoriten" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Favoritos" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -14402,6 +16808,12 @@ "value" : "Favoriten und Knoten mit aktuellen Nachrichten werden oben in der Kontaktliste angezeigt." } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Los favoritos y los nodos con mensajes recientes aparecen en la parte superior de la lista de contactos." + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -14442,6 +16854,12 @@ "value" : "Letzte Position eines Knotens holen" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Obtener la última posición de un nodo determinado" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -14470,6 +16888,12 @@ }, "Fifteen Minutes" : { "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "quince minutos" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -14504,6 +16928,12 @@ "value" : "Fünfzehn Sekunden" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "quince segundos" + } + }, "fr" : { "stringUnit" : { "state" : "translated", @@ -14562,6 +16992,12 @@ }, "File Storage" : { "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Almacenamiento de archivos" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -14591,6 +17027,12 @@ "Files Available" : { "comment" : "Data source label when files exist but none are active", "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Archivos disponibles" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -14607,6 +17049,12 @@ "value" : "Filtere die Knotenliste und die Mesh-Karte nach der Nähe zu deinem Handy." } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Filtre la lista de nodos y el mapa de malla según la proximidad a su teléfono." + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -14623,6 +17071,12 @@ "value" : "Kontakt suchen" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "encontrar un contacto" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -14663,6 +17117,12 @@ "value" : "Einen Knoten finden" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Encuentra un nodo" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -14703,6 +17163,12 @@ "value" : "Beenden" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Finalizar" + } + }, "fr" : { "stringUnit" : { "state" : "translated", @@ -14768,6 +17234,12 @@ "value" : "Ziel" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Finalizar" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -14784,6 +17256,12 @@ "value" : "Firmware" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "firmware" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -14818,6 +17296,12 @@ }, "Firmware update docs" : { "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Documentos de actualización de firmware" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -14858,6 +17342,12 @@ "value" : "Firmwareaktualisierungen" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Actualizaciones de firmware" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -14898,6 +17388,12 @@ "value" : "Firmware Version" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Versión de firmware" + } + }, "fr" : { "stringUnit" : { "state" : "translated", @@ -14956,6 +17452,12 @@ }, "First heard" : { "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "escuchado por primera vez" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -14996,6 +17498,12 @@ "value" : "Fünf Stunden" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "cinco horas" + } + }, "fr" : { "stringUnit" : { "state" : "translated", @@ -15060,6 +17568,12 @@ "value" : "Fünf Minuten" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "cinco minutos" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -15094,6 +17608,12 @@ "value" : "Fünf Sekunden" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "cinco segundos" + } + }, "fr" : { "stringUnit" : { "state" : "translated", @@ -15158,6 +17678,12 @@ "value" : "Feste PIN" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Pasador fijo" + } + }, "fr" : { "stringUnit" : { "state" : "translated", @@ -15216,6 +17742,12 @@ }, "Fixed Position" : { "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Posición fija" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -15244,6 +17776,12 @@ }, "Flip Screen" : { "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Voltear pantalla" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -15272,6 +17810,12 @@ }, "Flip screen vertically" : { "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Voltear la pantalla verticalmente" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -15306,6 +17850,12 @@ "value" : "Folgen" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Seguir" + } + }, "fr" : { "stringUnit" : { "state" : "translated", @@ -15370,6 +17920,12 @@ "value" : "Folgen mit Steuerkurs" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Seguir con encabezado" + } + }, "fr" : { "stringUnit" : { "state" : "translated", @@ -15428,6 +17984,12 @@ }, "For all Mqtt functionality other than the map report you must also set uplink and downlink for each channel you want to bridge over Mqtt." : { "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Para todas las funciones de Mqtt además del informe de mapa, también debe configurar el enlace ascendente y descendente para cada canal que desee conectar a través de Mqtt." + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -15468,6 +18030,12 @@ "value" : "Für alle" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Para todos" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -15502,6 +18070,12 @@ "value" : "Für mich" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Para mí" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -15536,6 +18110,12 @@ "value" : "Achtundvierzig Stunden" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "cuarenta y ocho horas" + } + }, "fr" : { "stringUnit" : { "state" : "translated", @@ -15600,6 +18180,12 @@ "value" : "Fündundvierzig Sekunden" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "cuarenta y cinco segundos" + } + }, "fr" : { "stringUnit" : { "state" : "translated", @@ -15664,6 +18250,12 @@ "value" : "Vier Stunden" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "cuatro horas" + } + }, "fr" : { "stringUnit" : { "state" : "translated", @@ -15728,6 +18320,12 @@ "value" : "Vier Sekunden" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "cuatro segundos" + } + }, "fr" : { "stringUnit" : { "state" : "translated", @@ -15792,6 +18390,12 @@ "value" : "Frequenz" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Frecuencia" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -15820,6 +18424,12 @@ }, "Frequency Override" : { "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Anulación de frecuencia" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -15848,6 +18458,12 @@ }, "Frequency Slot" : { "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ranura de frecuencia" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -15876,6 +18492,12 @@ }, "Friendly name" : { "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Nombre amigable" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -15910,6 +18532,12 @@ }, "Friendly name used to format message sent to mesh. Example: A name \"Motion\" would result in a message \"Motion detected\"" : { "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Nombre descriptivo utilizado para formatear el mensaje enviado a la malla. Ejemplo: un nombre \"Movimiento\" daría como resultado un mensaje \"Movimiento detectado\"." + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -15944,6 +18572,12 @@ }, "From Radio (RX): %lld" : { "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Desde radio (RX): %lld" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -15954,6 +18588,12 @@ }, "Full Support" : { "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Soporte completo" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -15988,6 +18628,12 @@ }, "Generate a new private key to replace the one currently in use. The public key will automatically be regenerated from your private key." : { "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Genere una nueva clave privada para reemplazar la que está actualmente en uso. La clave pública se regenerará automáticamente a partir de su clave privada." + } + }, "ja" : { "stringUnit" : { "state" : "translated", @@ -16010,6 +18656,12 @@ "value" : "QR Code Erzeugen" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Generar código QR" + } + }, "fr" : { "stringUnit" : { "state" : "translated", @@ -16068,6 +18720,12 @@ }, "Get custom waterproof solar and detection sensor router nodes, aluminium desktop nodes and rugged handsets." : { "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Obtenga nodos de enrutador de sensores de detección y solares impermeables personalizados, nodos de escritorio de aluminio y teléfonos resistentes." + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -16102,6 +18760,12 @@ "value" : "Knotenposition ermitteln" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Obtener la posición del nodo" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -16136,6 +18800,12 @@ }, "Get NRF DFU from the App Store" : { "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Obtenga NRF DFU en la App Store" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -16176,6 +18846,12 @@ "value" : "Los geht's" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "empezar" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -16186,6 +18862,12 @@ }, "Get the latest stable firmware" : { "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Obtenga el firmware estable más reciente" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -16220,6 +18902,12 @@ }, "GitHub Repository" : { "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Repositorio GitHub" + } + }, "ja" : { "stringUnit" : { "state" : "translated", @@ -16236,6 +18924,12 @@ }, "Good" : { "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Bien" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -16264,6 +18958,12 @@ }, "GPIO" : { "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "GPIO" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -16298,6 +18998,12 @@ }, "GPIO Output Duration" : { "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Duración de la salida GPIO" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -16326,6 +19032,12 @@ }, "GPIO pin for rotary encoder A port." : { "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Pin GPIO para el puerto A del codificador rotatorio." + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -16354,6 +19066,12 @@ }, "GPIO pin for rotary encoder B port." : { "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Pin GPIO para el puerto B del codificador rotatorio." + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -16382,6 +19100,12 @@ }, "GPIO pin for rotary encoder Press port." : { "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Pin GPIO para codificador rotatorio Puerto de prensa." + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -16410,6 +19134,12 @@ }, "GPIO Pin to monitor" : { "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Pin GPIO para monitorear" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -16438,6 +19168,12 @@ }, "GPS EN GPIO" : { "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "GPS EN GPIO" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -16472,6 +19208,12 @@ }, "GPS Receive GPIO" : { "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Recepción GPS GPIO" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -16506,6 +19248,12 @@ }, "GPS Transmit GPIO" : { "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Transmisión GPS GPIO" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -16546,6 +19294,12 @@ "value" : "Gruppennachricht" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Mensaje grupal" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -16580,6 +19334,12 @@ }, "Gusts %@" : { "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ráfagas %@" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -16608,6 +19368,12 @@ }, "HaHa" : { "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ja ja" + } + }, "he" : { "stringUnit" : { "state" : "translated", @@ -16648,6 +19414,12 @@ }, "Hard Reset" : { "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Restablecimiento completo" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -16658,6 +19430,12 @@ }, "Hardware" : { "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Hardware" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -16692,6 +19470,12 @@ }, "Hazardous" : { "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Peligroso" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -16720,6 +19504,12 @@ }, "Heading" : { "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Título" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -16754,6 +19544,12 @@ "value" : "Kurs: %@" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Título: %@" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -16788,6 +19584,12 @@ "value" : "Gehört" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Escuchó" + } + }, "fr" : { "stringUnit" : { "state" : "translated", @@ -16852,6 +19654,12 @@ "value" : "Herz" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Corazón" + } + }, "fr" : { "stringUnit" : { "state" : "translated", @@ -16910,6 +19718,12 @@ }, "Hide alerts" : { "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ocultar alertas" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -16944,6 +19758,12 @@ }, "Hide Alerts" : { "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ocultar alertas" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -16978,6 +19798,12 @@ "value" : "HOCH" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "ALTO" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -17018,6 +19844,12 @@ "value" : "Wandern" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Senderismo" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -17052,6 +19884,12 @@ }, "History Return Max" : { "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Historial Retorno Max" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -17080,6 +19918,12 @@ }, "History Return Window" : { "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ventana de retorno del historial" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -17114,6 +19958,12 @@ "value" : "Hops Entfernt" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "salta lejos" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -17148,6 +19998,12 @@ "value" : "Hops Entfernt %d" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Salta lejos %d" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -17182,6 +20038,12 @@ "value" : "Hops Entfernt:" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Saltos lejos:" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -17216,6 +20078,12 @@ "value" : "Hops Entfernt: %d" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Saltos lejos: %d" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -17250,6 +20118,12 @@ "value" : "Stunde" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Hora" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -17284,6 +20158,12 @@ }, "Hourly Duty Cycle" : { "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ciclo de trabajo por hora" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -17312,6 +20192,12 @@ }, "How long the screen remains on after the user button is pressed or messages are received." : { "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Cuánto tiempo permanece encendida la pantalla después de presionar el botón de usuario o recibir mensajes." + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -17346,6 +20232,12 @@ }, "How often device metrics are sent out over the mesh. Default is 30 minutes." : { "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Con qué frecuencia se envían las métricas del dispositivo a través de la malla. El valor predeterminado es 30 minutos." + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -17380,6 +20272,12 @@ }, "How often environment metrics are sent out over the mesh. Default is 30 minutes." : { "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Con qué frecuencia se envían métricas ambientales a través de la malla. El valor predeterminado es 30 minutos." + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -17414,6 +20312,12 @@ }, "How often power metrics are sent out over the mesh. Default is 30 minutes." : { "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Con qué frecuencia se envían métricas de potencia a través de la malla. El valor predeterminado es 30 minutos." + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -17448,6 +20352,12 @@ }, "How often should we try to get a GPS position." : { "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "¿Con qué frecuencia debemos intentar obtener una posición GPS?" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -17482,6 +20392,12 @@ }, "How often to send detection sensor state to mesh regardless of detection. Default is Never." : { "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Con qué frecuencia enviar el estado del sensor de detección a la malla independientemente de la detección. El valor predeterminado es Nunca." + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -17522,6 +20438,12 @@ "value" : "How often we can send a message to the mesh when people are detected." } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Con qué frecuencia podemos enviar un mensaje a la malla cuando se detectan personas." + } + }, "he" : { "stringUnit" : { "state" : "translated", @@ -17580,6 +20502,12 @@ "value" : "Wie wird die Firmware aktualisiert" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Cómo actualizar el firmware" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -17614,6 +20542,12 @@ }, "Hum" : { "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Tararear" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -17648,6 +20582,12 @@ "value" : "Luftfeuchtigkeit" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Humedad" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -17676,6 +20616,12 @@ }, "Hybrid" : { "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Híbrido" + } + }, "fr" : { "stringUnit" : { "state" : "translated", @@ -17734,6 +20680,12 @@ }, "Hybrid Flyover" : { "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Paso elevado híbrido" + } + }, "fr" : { "stringUnit" : { "state" : "translated", @@ -17792,6 +20744,12 @@ }, "I have read and understand the above. I voluntarily consent to the unencrypted transmission of my node data via MQTT." : { "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "He leído y entiendo lo anterior. Doy mi consentimiento voluntariamente para la transmisión sin cifrar de los datos de mi nodo a través de MQTT." + } + }, "ja" : { "stringUnit" : { "state" : "translated", @@ -17814,6 +20772,12 @@ }, "IAQ" : { "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "IAQ" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -17848,6 +20812,12 @@ }, "IAQ " : { "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "IAQ" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -17882,6 +20852,12 @@ }, "IAQ %lld" : { "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "IAQ %lld" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -17922,6 +20898,12 @@ "value" : "Emoji" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Icono" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -17956,6 +20938,12 @@ }, "Icons" : { "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Iconos" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -17966,6 +20954,12 @@ }, "If DOP is set, use HDOP / VDOP values instead of PDOP" : { "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Si se configura DOP, use valores HDOP/VDOP en lugar de PDOP" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -18000,6 +20994,12 @@ }, "If enabled, the 'output' Pin will be pulled active high, disabled means active low." : { "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Si está habilitado, el pin de 'salida' se activará alto, deshabilitado significa activo bajo." + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -18034,6 +21034,12 @@ }, "If it is hard to access your device's reset button enter DFU mode here." : { "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Si le resulta difícil acceder al botón de reinicio de su dispositivo, ingrese al modo DFU aquí." + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -18068,6 +21074,12 @@ }, "If set, any packets you send will be echoed back to your device." : { "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Si está configurado, cualquier paquete que envíe se enviará a su dispositivo." + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -18102,6 +21114,12 @@ }, "If the default region topic is too busy you can choose a more local topic." : { "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Si el tema de la región predeterminada está demasiado ocupado, puede elegir un tema más local." + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -18136,6 +21154,12 @@ }, "Ignore MQTT" : { "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ignorar MQTT" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -18170,6 +21194,12 @@ }, "Ignore Node" : { "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ignorar nodo" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -18204,6 +21234,12 @@ }, "Ignored" : { "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "ignorado" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -18238,6 +21274,12 @@ }, "Ignores observed messages from foreign meshes like Local Only, but takes it step further by also ignoring messages from nodes not already in the node's known list." : { "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ignora los mensajes observados de mallas externas como Solo local, pero va un paso más allá al ignorar también los mensajes de nodos que aún no están en la lista conocida del nodo." + } + }, "ja" : { "stringUnit" : { "state" : "translated", @@ -18260,6 +21302,12 @@ }, "Ignores observed messages from foreign meshes that are open or those which it cannot decrypt. Only rebroadcasts message on the nodes local primary / secondary channels." : { "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ignora los mensajes observados de mallas externas que están abiertas o aquellas que no puede descifrar. Sólo retransmite mensajes en los canales primarios/secundarios locales del nodo." + } + }, "ja" : { "stringUnit" : { "state" : "translated", @@ -18288,6 +21336,12 @@ "value" : "Route importieren" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ruta de importación" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -18322,6 +21376,12 @@ }, "In addition to Config, Keys and BLE bonds will be wiped" : { "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Además de la configuración, se borrarán las claves y los enlaces BLE." + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -18338,6 +21398,12 @@ "value" : "Include" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Incluir" + } + }, "fr" : { "stringUnit" : { "state" : "translated", @@ -18402,6 +21468,12 @@ "value" : "Eingehende Nachrichten" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Mensajes entrantes" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -18418,6 +21490,12 @@ "value" : "Unvollständig" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Incompleto" + } + }, "fr" : { "stringUnit" : { "state" : "translated", @@ -18452,6 +21530,12 @@ }, "India" : { "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "India" + } + }, "ja" : { "stringUnit" : { "state" : "translated", @@ -18474,6 +21558,12 @@ }, "Indoor Air Quality" : { "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Calidad del aire interior" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -18508,6 +21598,12 @@ }, "Indoor Air Quality (IAQ)" : { "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Calidad del aire interior (IAQ)" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -18548,6 +21644,12 @@ "value" : "Router - Mesh Pakete werden bevorzugt über diesen Knoten gerouted. Dieser Knoten wird nicht von einer Client App benutzt. WLAN, Bluetooth und Display sind aus." } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Nodo de infraestructura únicamente en una torre o cima de una montaña. No debe usarse para techos o nodos móviles. Necesita una cobertura excepcional. Visible en la lista de nodos." + } + }, "fr" : { "stringUnit" : { "state" : "translated", @@ -18606,6 +21708,12 @@ }, "Inputs" : { "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Entradas" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -18646,6 +21754,12 @@ "value" : "Ungültiger Dateiinhalt. Bitte überprüfe das Dateiformat." } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Contenido del archivo no válido. Por favor verifique el formato del archivo." + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -18656,6 +21770,12 @@ }, "Inverted top bar for 2 Color display" : { "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Barra superior invertida para pantalla de 2 colores" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -18690,6 +21810,12 @@ }, "Japan" : { "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Japón" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -18718,6 +21844,12 @@ }, "JSON Enabled" : { "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "JSON habilitado" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -18752,6 +21884,12 @@ }, "JSON mode is a limited, unencrypted MQTT output for locally integrating with home assistant" : { "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "El modo JSON es una salida MQTT limitada y sin cifrar para la integración local con el asistente doméstico" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -18786,6 +21924,12 @@ }, "Jump to present" : { "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Saltar al presente" + } + }, "ja" : { "stringUnit" : { "state" : "translated", @@ -18814,6 +21958,12 @@ "value" : "Schlüssel" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Llave" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -18848,6 +21998,12 @@ }, "Key Backup" : { "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Copia de seguridad clave" + } + }, "ja" : { "stringUnit" : { "state" : "translated", @@ -18864,6 +22020,12 @@ }, "Key Mapping" : { "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Mapeo de claves" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -18898,6 +22060,12 @@ "value" : "Schlüsselgröße" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Tamaño de clave" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -18926,6 +22094,12 @@ }, "Korea" : { "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Corea" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -18960,6 +22134,12 @@ "value" : "Zuletzt gehört" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Escuchado por última vez" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -19007,6 +22187,12 @@ "value" : "Breitengrad" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Latitud" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -19041,6 +22227,12 @@ }, "Latitude in degrees (e.g., 37.7749)" : { "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Latitud en grados (por ejemplo, 37,7749)" + } + }, "ja" : { "stringUnit" : { "state" : "translated", @@ -19057,6 +22249,12 @@ }, "Latitude must be between -90 and 90 degrees" : { "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "La latitud debe estar entre -90 y 90 grados." + } + }, "ja" : { "stringUnit" : { "state" : "translated", @@ -19073,6 +22271,12 @@ }, "LED Heartbeat" : { "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Latido del corazón LED" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -19113,6 +22317,12 @@ "value" : "LED Status" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Estado del LED" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -19153,6 +22363,12 @@ "value" : "Links" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Izquierda" + } + }, "fr" : { "stringUnit" : { "state" : "translated", @@ -19217,6 +22433,12 @@ "value" : "Level" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Nivel" + } + }, "fr" : { "stringUnit" : { "state" : "translated", @@ -19275,6 +22497,12 @@ }, "Licensed Operator" : { "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Operador Licenciado" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -19309,6 +22537,12 @@ }, "Limit all periodic broadcast intervals especially telemetry and position. If you need to increase hops, do it on nodes at the edges, not the ones in the middle. MQTT is not advised when you are duty cycle restricted because the gateway node is then doing all the work." : { "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Limite todos los intervalos de transmisión periódica, especialmente la telemetría y la posición. Si necesita aumentar los saltos, hágalo en los nodos de los bordes, no en los del medio. No se recomienda MQTT cuando el ciclo de trabajo está restringido porque el nodo de puerta de enlace está haciendo todo el trabajo." + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -19343,6 +22577,12 @@ }, "Line Series" : { "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Serie de línea" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -19371,6 +22611,12 @@ }, "Loading Logs. . ." : { "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Cargando registros. . ." + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -19407,6 +22653,12 @@ "comment" : "A label displayed above the options for local network access.", "isCommentAutoGenerated" : true, "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Acceso a la red local" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -19423,6 +22675,12 @@ "value" : "Standort:" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ubicación:" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -19463,6 +22721,12 @@ "value" : "Gesperrt" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "bloqueado" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -19497,6 +22761,12 @@ }, "Log Levels" : { "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Niveles de registro" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -19537,6 +22807,12 @@ "value" : "Logging" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Explotación florestal" + } + }, "fr" : { "stringUnit" : { "state" : "translated", @@ -19595,6 +22871,12 @@ }, "Logs" : { "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Registros" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -19629,6 +22911,12 @@ }, "Logs:" : { "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Registros:" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -19669,6 +22957,12 @@ "value" : "Langer Name" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Nombre largo" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -19709,6 +23003,12 @@ "value" : "Durch langes Gedrückthalten kannst du den Kontakt zu deinen Favoriten hinzufügen, stumm schalten oder eine Unterhaltung löschen." } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Mantenga presionado para marcar como favorito, silenciar el contacto o eliminar una conversación." + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -19743,6 +23043,12 @@ }, "Long Range - Fast" : { "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Largo alcance - Rápido" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -19771,6 +23077,12 @@ }, "Long Range - Moderate" : { "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Largo alcance - Moderado" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -19799,6 +23111,12 @@ }, "Long Range - Slow" : { "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Largo alcance - Lento" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -19833,6 +23151,12 @@ "value" : "Längengrad" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Longitud" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -19867,6 +23191,12 @@ }, "Longitude in degrees (e.g., -122.4194)" : { "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Longitud en grados (p. ej., -122,4194)" + } + }, "ja" : { "stringUnit" : { "state" : "translated", @@ -19883,6 +23213,12 @@ }, "Longitude must be between -180 and 180 degrees" : { "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "La longitud debe estar entre -180 y 180 grados." + } + }, "ja" : { "stringUnit" : { "state" : "translated", @@ -19905,6 +23241,12 @@ "value" : "LoRa" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "lora" + } + }, "fr" : { "stringUnit" : { "state" : "translated", @@ -19969,6 +23311,12 @@ "value" : "LoRa Einstellungen" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Configuración LoRa" + } + }, "fr" : { "stringUnit" : { "state" : "translated", @@ -20027,6 +23375,12 @@ }, "LoRa Config Changes:" : { "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Cambios en la configuración de LoRa:" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -20043,6 +23397,12 @@ "value" : "LoRa config empfangen: %@" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Configuración de LoRa recibida: %@" + } + }, "fr" : { "stringUnit" : { "state" : "translated", @@ -20107,6 +23467,12 @@ "value" : "Tracker" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Objetos perdidos y encontrados" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -20141,6 +23507,12 @@ }, "LOW" : { "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "BAJO" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -20175,6 +23547,12 @@ "value" : "Niedriger Akkustand" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Batería baja" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -20191,6 +23569,12 @@ "value" : "M5 Stack Card KB / RAK Tastenfeld" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Tarjeta de pila M5 Teclado KB / RAK" + } + }, "fr" : { "stringUnit" : { "state" : "translated", @@ -20231,6 +23615,12 @@ }, "Malaysia 433MHz" : { "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Malasia 433MHz" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -20259,6 +23649,12 @@ }, "Malaysia 919MHz" : { "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Malasia 919MHz" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -20293,6 +23689,12 @@ "value" : "Kanäle verwalten" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Administrar canales" + } + }, "fr" : { "stringUnit" : { "state" : "translated", @@ -20352,6 +23754,12 @@ "Manage custom map overlays" : { "comment" : "Subtitle for map data management", "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Administrar superposiciones de mapas personalizados" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -20393,6 +23801,12 @@ "value" : "Kartendaten verwalten" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Administrar datos de mapas" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -20403,6 +23817,12 @@ }, "Managed Device" : { "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Dispositivo administrado" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -20437,6 +23857,12 @@ }, "Manual" : { "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Manual" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -20459,6 +23885,12 @@ "value" : "Manuelle Konfiguration" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Configuración manual" + } + }, "fr" : { "stringUnit" : { "state" : "translated", @@ -20517,6 +23949,12 @@ }, "Manual connection string" : { "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Cadena de conexión manual" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -20530,6 +23968,12 @@ }, "Map Data" : { "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Datos del mapa" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -20546,6 +23990,12 @@ "value" : "Kartenoptionen" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Opciones de mapa" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -20586,6 +24036,12 @@ "value" : "Karten-Overlays" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Superposiciones de mapas" + } + }, "fr" : { "stringUnit" : { "state" : "translated", @@ -20644,6 +24100,12 @@ }, "Map Publish Interval" : { "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Intervalo de publicación de mapas" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -20678,6 +24140,12 @@ }, "Map Report" : { "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Informe de mapa" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -20718,6 +24186,12 @@ "value" : "Maximale Wiederholungen erreicht" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Retransmisión máxima alcanzada" + } + }, "fr" : { "stringUnit" : { "state" : "translated", @@ -20776,6 +24250,12 @@ }, "Medium Range - Fast" : { "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Rango medio - Rápido" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -20804,6 +24284,12 @@ }, "Medium Range - Slow" : { "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Rango medio - Lento" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -20832,6 +24318,12 @@ }, "Mesh activity update" : { "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Actualización de actividad de malla" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -20866,6 +24358,12 @@ "value" : "Mesh Live Aktivität" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Actividad en vivo de malla" + } + }, "fr" : { "stringUnit" : { "state" : "translated", @@ -20930,6 +24428,12 @@ "value" : "Mesh Karte" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Mapa de malla" + } + }, "fr" : { "stringUnit" : { "state" : "translated", @@ -20994,6 +24498,12 @@ "value" : "Standort auf der Mesh-Karte" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ubicación del mapa de malla" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -21004,6 +24514,12 @@ }, "Meshtastic" : { "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Meshtástico" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -21023,6 +24539,12 @@ "value" : "Meshtastic Knoten %@ hat Kanäle mit dir geteilt" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Meshtastic Node %@ ha compartido canales contigo" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -21057,6 +24579,12 @@ "value" : "Meshtastic verwendet den Standort deines Handys, um eine Reihe von Funktionen zu ermöglichen. Du kannst deine Standortberechtigungen jederzeit in den Einstellungen aktualisieren." } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Meshtastic utiliza la ubicación de su teléfono para habilitar una serie de funciones. Puede actualizar sus permisos de ubicación en cualquier momento desde la configuración." + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -21067,6 +24595,12 @@ }, "Meshtastic® Copyright Meshtastic LLC" : { "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Meshtastic® Copyright Meshtastic LLC" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -21101,6 +24635,12 @@ "value" : "Nachricht" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Mensaje" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -21135,6 +24675,12 @@ "value" : "Nachrichteninhalt überschreitet 200 Bytes." } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "El contenido del mensaje supera los 200 bytes." + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -21169,6 +24715,12 @@ "value" : "Nachrichtendetails" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Detalles del mensaje" + } + }, "fr" : { "stringUnit" : { "state" : "translated", @@ -21233,6 +24785,12 @@ "value" : "Nachricht von der Textnachricht-App empfangen." } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Mensaje recibido de la aplicación de mensajes de texto." + } + }, "fr" : { "stringUnit" : { "state" : "translated", @@ -21292,6 +24850,12 @@ "Message Size" : { "comment" : "VoiceOver label for message size", "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Tamaño del mensaje" + } + }, "ja" : { "stringUnit" : { "state" : "translated", @@ -21308,6 +24872,12 @@ }, "Message Status Options" : { "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Opciones de estado del mensaje" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -21342,6 +24912,12 @@ "value" : "Nachrichten" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Mensajes" + } + }, "he" : { "stringUnit" : { "state" : "translated", @@ -21400,6 +24976,12 @@ "value" : "Nachrichten getrennt mit |" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Los mensajes se separan con |" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -21428,6 +25010,12 @@ }, "Messaging" : { "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Mensajería" + } + }, "ja" : { "stringUnit" : { "state" : "translated", @@ -21444,6 +25032,12 @@ }, "Metric" : { "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Métrico" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -21478,6 +25072,12 @@ "value" : "Mittag" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Uno" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -21512,6 +25112,12 @@ "value" : "Minimum Distanz" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Distancia mínima" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -21546,6 +25152,12 @@ "value" : "Minimum Intervall" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Intervalo mínimo" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -21574,6 +25186,12 @@ }, "Minimum time between detection broadcasts" : { "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Tiempo mínimo entre transmisiones de detección" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -21602,6 +25220,12 @@ }, "Mininum time between detection broadcasts. Default is 45 seconds." : { "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Tiempo mínimo entre transmisiones de detección. El valor predeterminado es 45 segundos." + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -21636,6 +25260,12 @@ "value" : "Modus" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Modo" + } + }, "fr" : { "stringUnit" : { "state" : "translated", @@ -21694,6 +25324,12 @@ }, "Model" : { "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Modelo" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -21728,6 +25364,12 @@ }, "Moderate" : { "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Moderado" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -21768,6 +25410,12 @@ "value" : "Modul Konfiguration" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Configuración del módulo" + } + }, "fr" : { "stringUnit" : { "state" : "translated", @@ -21832,6 +25480,12 @@ "value" : "Morgen" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Mañana" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -21866,6 +25520,12 @@ "value" : "Die meisten Daten in deinem Mesh werden über den primären Kanal gesendet. Du kannst sekundäre Kanäle einrichten, um zusätzliche Nachrichtengruppen zu erstellen, die durch ihren eigenen Schlüssel gesichert sind. [Tipps zur Kanalkonfiguration](https://meshtastic.org/docs/configuration/radio/channels/)" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "La mayoría de los datos de su malla se envían a través del canal principal. Puede configurar canales secundarios para crear grupos de mensajería adicionales protegidos por su propia clave. [Consejos de configuración de canales](https://meshtastic.org/docs/configuration/tips/)" + } + }, "fr" : { "stringUnit" : { "state" : "translated", @@ -21924,6 +25584,12 @@ }, "MQTT" : { "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "MQTT" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -21964,6 +25630,12 @@ "value" : "MQTT Client Proxy" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Proxy de cliente MQTT" + } + }, "fr" : { "stringUnit" : { "state" : "translated", @@ -22028,6 +25700,12 @@ "value" : "MQTT Konfiguration" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Configuración MQTT" + } + }, "fr" : { "stringUnit" : { "state" : "translated", @@ -22092,6 +25770,12 @@ "value" : "MQTT Modulkonfiguration empfangen: %@" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Configuración del módulo MQTT recibida: %@" + } + }, "fr" : { "stringUnit" : { "state" : "translated", @@ -22156,6 +25840,12 @@ "value" : "Multiplier" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Multiplicador" + } + }, "he" : { "stringUnit" : { "state" : "translated", @@ -22202,6 +25892,12 @@ }, "Must be a single emoji" : { "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Debe ser un solo emoji" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -22236,6 +25932,12 @@ "value" : "MyInfo empfangen: %@" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Mi información recibida: %@" + } + }, "fr" : { "stringUnit" : { "state" : "translated", @@ -22295,6 +25997,12 @@ "Nag timeout" : { "extractionState" : "stale", "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Se acabó el tiempo" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -22322,7 +26030,14 @@ } }, "Nag Timeout" : { - + "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Tiempo agotado" + } + } + } }, "Name" : { "localizations" : { @@ -22332,6 +26047,12 @@ "value" : "Name" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Nombre" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -22372,6 +26093,12 @@ "value" : "Name muss kürzer als 30 Bytes sein" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "El nombre debe tener menos de 30 bytes." + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -22406,6 +26133,12 @@ }, "Navigate to node" : { "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Navegar al nodo" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -22434,6 +26167,12 @@ }, "Nearby Topics" : { "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Temas cercanos" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -22468,6 +26207,12 @@ "value" : "Netzwerk" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Red" + } + }, "fr" : { "stringUnit" : { "state" : "translated", @@ -22532,6 +26277,12 @@ "value" : "Netzwerkeinstellungen" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Configuración de red" + } + }, "fr" : { "stringUnit" : { "state" : "translated", @@ -22596,6 +26347,12 @@ "value" : "Netzwerkkonfiguration empfangen: %@" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Configuración de red recibida: %@" + } + }, "fr" : { "stringUnit" : { "state" : "translated", @@ -22654,6 +26411,12 @@ }, "Network Status Orange" : { "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Estado de la red naranja" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -22688,6 +26451,12 @@ }, "Network Status Red" : { "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Estado de la red Rojo" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -22722,6 +26491,12 @@ }, "New Node" : { "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Nuevo nodo" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -22750,6 +26525,12 @@ }, "New Node has been discovered" : { "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Se ha descubierto un nuevo nodo." + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -22784,6 +26565,12 @@ "value" : "Neue Knoten" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Nuevos nodos" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -22794,6 +26581,12 @@ }, "New Zealand 865MHz" : { "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Nueva Zelanda 865MHz" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -22828,6 +26621,12 @@ "value" : "Neuere Firmware ist verfügbar" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Hay un firmware más nuevo disponible" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -22868,6 +26667,12 @@ "value" : "Nacht" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Noche" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -22902,6 +26707,12 @@ "value" : "NMEA Positionen" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Posiciones NMEA" + } + }, "fr" : { "stringUnit" : { "state" : "translated", @@ -22966,6 +26777,12 @@ "value" : "Kein Kanal" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Sin canal" + } + }, "fr" : { "stringUnit" : { "state" : "translated", @@ -23030,6 +26847,12 @@ "value" : "Kein verbundener Knoten" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ningún nodo conectado" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -23065,6 +26888,12 @@ "value" : "Keine Daten vorhanden" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Sin datos" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -23105,6 +26934,12 @@ "value" : "Kein Gerät verbunden" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ningún dispositivo conectado" + } + }, "fr" : { "stringUnit" : { "state" : "translated", @@ -23163,6 +26998,12 @@ }, "No Device Metrics" : { "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Sin métricas de dispositivo" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -23191,6 +27032,12 @@ }, "No Environment Metrics" : { "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Sin métricas ambientales" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -23231,6 +27078,12 @@ "value" : "Keine Dateien hochgeladen" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "No se subieron archivos" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -23247,6 +27100,12 @@ "value" : "Keine Schnittstelle" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Sin interfaz" + } + }, "fr" : { "stringUnit" : { "state" : "translated", @@ -23306,6 +27165,12 @@ "No map data files uploaded" : { "comment" : "Message when no files are uploaded", "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "No se han subido archivos de datos de mapas" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -23316,6 +27181,12 @@ }, "No PAX Counter Logs" : { "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Sin registros de contador de PAX" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -23356,6 +27227,12 @@ "value" : "Keine PIN (geht einfach)" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Sin PIN (simplemente funciona)" + } + }, "fr" : { "stringUnit" : { "state" : "translated", @@ -23420,6 +27297,12 @@ "value" : "Keine Positionen" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Sin posiciones" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -23448,6 +27331,12 @@ }, "No Power Metrics" : { "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Sin métricas de energía" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -23482,6 +27371,12 @@ "value" : "Keine Antwort" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Sin respuesta" + } + }, "fr" : { "stringUnit" : { "state" : "translated", @@ -23546,6 +27441,12 @@ "value" : "Keine Route" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Sin ruta" + } + }, "fr" : { "stringUnit" : { "state" : "translated", @@ -23610,6 +27511,12 @@ "value" : "Knoten" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Nodo" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -23644,6 +27551,12 @@ "value" : "Node Core Data Backup %1$@/%2$@ - %3$@ - %4$@" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Copia de seguridad de datos principales del nodo %@/%@ - %@ - %@" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -23684,6 +27597,12 @@ "value" : "Knoten hat keine Position" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "El nodo no tiene posiciones" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -23718,6 +27637,12 @@ "value" : "Knoten Historie" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Historia del nodo" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -23746,6 +27671,12 @@ }, "Node Info Broadcast Interval" : { "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Intervalo de transmisión de información de nodo" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -23780,6 +27711,12 @@ "value" : "Knotenkarte" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Mapa de nodos" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -23814,6 +27751,12 @@ "value" : "Knotennummer" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Número de nodo" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -23854,6 +27797,12 @@ "value" : "Knoten" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Nodos" + } + }, "fr" : { "stringUnit" : { "state" : "translated", @@ -23912,6 +27861,12 @@ "value" : "Knoten (%@)" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Nodos (%@)" + } + }, "fr" : { "stringUnit" : { "state" : "translated", @@ -23976,6 +27931,12 @@ "value" : "Keins" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ninguno" + } + }, "fr" : { "stringUnit" : { "state" : "translated", @@ -24034,6 +27995,12 @@ }, "Not a valid route file" : { "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "No es un archivo de ruta válido" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -24068,6 +28035,12 @@ "value" : "Nicht authorisiert" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "No autorizado" + } + }, "fr" : { "stringUnit" : { "state" : "translated", @@ -24124,8 +28097,24 @@ } } }, + "Not Connected" : { + "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "No conectado" + } + } + } + }, "Not Present" : { "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "No presente" + } + }, "fr" : { "stringUnit" : { "state" : "translated", @@ -24184,6 +28173,12 @@ "value" : "Knoten" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Notas" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -24218,6 +28213,12 @@ "value" : "Mitteilungen für Kanal- und Direktnachrichten." } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Notificaciones por canal y mensajes directos." + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -24234,6 +28235,12 @@ "value" : "Mitteilungen bei niedrigem Akkustand des verbundenen Funkgeräts." } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Notificaciones de alertas de batería baja para el dispositivo conectado." + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -24250,6 +28257,12 @@ "value" : "Mitteilungen für neu entdeckte Knoten." } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Notificaciones para nodos recién descubiertos." + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -24266,6 +28279,12 @@ "value" : "Anzahl Hops" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Número de saltos" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -24300,6 +28319,12 @@ "value" : "Anzahl Einträge" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Número de registros" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -24334,6 +28359,12 @@ "value" : "Anzahl Satelliten" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Número de satélites" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -24362,6 +28393,12 @@ }, "Ok" : { "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "De acuerdo" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -24378,6 +28415,12 @@ "value" : "Ok" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "DE ACUERDO" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -24406,6 +28449,12 @@ }, "Ok to MQTT" : { "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ok para MQTT" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -24446,6 +28495,12 @@ "value" : "OLED Typ" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Tipo OLED" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -24486,6 +28541,12 @@ "value" : "Nur beim Starten" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Sólo en el arranque" + } + }, "fr" : { "stringUnit" : { "state" : "translated", @@ -24544,6 +28605,12 @@ }, "Onboarding for licensed operators requires firmware 2.0.20 or greater. Make sure to refer to your local regulations and contact the local amateur frequency coordinators with questions." : { "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "La incorporación de operadores con licencia requiere firmware 2.0.20 o superior. Asegúrese de consultar las regulaciones locales y comuníquese con los coordinadores locales de frecuencias de aficionados si tiene preguntas." + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -24584,6 +28651,12 @@ "value" : "Eine Stunde" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Una hora" + } + }, "fr" : { "stringUnit" : { "state" : "translated", @@ -24648,6 +28721,12 @@ "value" : "Eine Minute" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "un minuto" + } + }, "fr" : { "stringUnit" : { "state" : "translated", @@ -24712,6 +28791,12 @@ "value" : "Eine Sekunde" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "un segundo" + } + }, "fr" : { "stringUnit" : { "state" : "translated", @@ -24776,6 +28861,12 @@ "value" : "Online" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "En línea" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -24804,6 +28895,12 @@ }, "Only permitted for SENSOR, TRACKER and TAK_TRACKER roles, this will inhibit all rebroadcasts, not unlike CLIENT_MUTE role." : { "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Solo permitido para los roles SENSOR, TRACKER y TAK_TRACKER, esto inhibirá todas las retransmisiones, al igual que el rol CLIENT_MUTE." + } + }, "ja" : { "stringUnit" : { "state" : "translated", @@ -24826,6 +28923,12 @@ }, "Only rebroadcasts packets from the core portnums: NodeInfo, Text, Position, Telemetry, and Routing." : { "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Solo retransmite paquetes desde los portnums principales: NodeInfo, Texto, Posición, Telemetría y Enrutamiento." + } + }, "ja" : { "stringUnit" : { "state" : "translated", @@ -24857,6 +28960,12 @@ "value" : "Einstellungen öffnen" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Abrir configuración" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -24891,6 +29000,12 @@ }, "Optimized for 2 color displays" : { "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Optimizado para pantallas de 2 colores" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -24925,6 +29040,12 @@ "value" : "Optimiert für ATAK-Systemkommunikation, verringert die Anzahl der Routineübertragungen." } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Optimizado para la comunicación del sistema ATAK, reduce las transmisiones de rutina." + } + }, "fr" : { "stringUnit" : { "state" : "translated", @@ -24983,6 +29104,12 @@ }, "Optional fields to include when assembling position messages. the more fields are included, the larger the message will be - leading to longer airtime and a higher risk of packet loss" : { "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Campos opcionales para incluir al ensamblar mensajes de posición. Cuantos más campos se incluyan, más grande será el mensaje, lo que llevará a un mayor tiempo de emisión y a un mayor riesgo de pérdida de paquetes." + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -25017,6 +29144,12 @@ }, "Optional GPIO" : { "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "GPIO opcional" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -25051,6 +29184,12 @@ "value" : "Optionen" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Opciones" + } + }, "fr" : { "stringUnit" : { "state" : "translated", @@ -25109,6 +29248,12 @@ }, "OS Log Entry Details" : { "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Detalles de entrada de registro del sistema operativo" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -25137,6 +29282,12 @@ }, "OTA Updates are not supported on this NRF Device." : { "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Las actualizaciones OTA no son compatibles con este dispositivo NRF." + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -25171,6 +29322,12 @@ }, "OTA Updates are not supported on your platform." : { "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Las actualizaciones OTA no son compatibles con su plataforma." + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -25205,6 +29362,12 @@ }, "Other data sources" : { "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Otras fuentes de datos" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -25239,6 +29402,12 @@ "value" : "Ausgabe von Echtzeit-Fehlersuchprotokollen über die serielle Schnittstelle, Anzeige und Export von positionskorrigierten Geräteprotokollen über Bluetooth." } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Genere registros de depuración en vivo a través de serie, vea y exporte registros de dispositivos redactados en posición a través de Bluetooth." + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -25267,6 +29436,12 @@ }, "Output pin buzzer GPIO " : { "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Zumbador de pin de salida GPIO" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -25295,6 +29470,12 @@ }, "Output pin GPIO" : { "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Pin de salida GPIO" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -25323,6 +29504,12 @@ }, "Output pin vibra GPIO" : { "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Pin de salida vibración GPIO" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -25351,6 +29538,12 @@ }, "Overlanding" : { "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Por tierra" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -25385,6 +29578,12 @@ }, "Override automatic OLED screen detection." : { "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Anule la detección automática de pantalla OLED." + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -25413,6 +29612,12 @@ }, "Override default screen layout." : { "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Anular el diseño de pantalla predeterminado." + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -25423,6 +29628,12 @@ }, "Packet Count" : { "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Recuento de paquetes" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -25439,6 +29650,12 @@ "value" : "Pairing Modus" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Modo de emparejamiento" + } + }, "fr" : { "stringUnit" : { "state" : "translated", @@ -25503,6 +29720,12 @@ "value" : "Passwort" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Contraseña" + } + }, "fr" : { "stringUnit" : { "state" : "translated", @@ -25567,6 +29790,12 @@ "value" : "Pause" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Pausa" + } + }, "fr" : { "stringUnit" : { "state" : "translated", @@ -25625,6 +29854,12 @@ }, "PAX Counter" : { "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Contador de pasajeros" + } + }, "he" : { "stringUnit" : { "state" : "translated", @@ -25677,6 +29912,12 @@ }, "PAX Counter Config" : { "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Configuración del contador PAX" + } + }, "he" : { "stringUnit" : { "state" : "translated", @@ -25723,6 +29964,12 @@ }, "PAX Counter config received: %@" : { "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Configuración del contador PAX recibida: %@" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -25757,6 +30004,12 @@ }, "PAX Counter Log" : { "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Registro de contador de PAX" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -25797,6 +30050,12 @@ "value" : "PAX Counter message received for: %@" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Mensaje del contador de PAX recibido de: %@" + } + }, "he" : { "stringUnit" : { "state" : "translated", @@ -25849,6 +30108,12 @@ }, "paxcounter.log" : { "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "paxcounter.log" + } + }, "ja" : { "stringUnit" : { "state" : "translated", @@ -25877,6 +30142,12 @@ "value" : "Verbundenen Knoten auf Werkseinstellungen zurücksetzen" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Realice un restablecimiento de fábrica en el nodo al que está conectado" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -25905,6 +30176,12 @@ }, "Philippines 433MHz" : { "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Filipinas 433MHz" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -25933,6 +30210,12 @@ }, "Philippines 868MHz" : { "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Filipinas 868MHz" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -25961,6 +30244,12 @@ }, "Philippines 915MHz" : { "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Filipinas 915MHz" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -25995,6 +30284,12 @@ "value" : "Telefon GPS" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "GPS del teléfono" + } + }, "fr" : { "stringUnit" : { "state" : "translated", @@ -26059,6 +30354,12 @@ "value" : "Standorteinstellungen" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ubicación del teléfono" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -26069,6 +30370,12 @@ }, "Pin %lld" : { "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Fijar %lld" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -26097,6 +30404,12 @@ }, "Pin A" : { "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Pin A" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -26125,6 +30438,12 @@ }, "Pin B" : { "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Pin B" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -26159,6 +30478,12 @@ "value" : "PKI-basierte Knotenadministration, benötigt Firmware Version 2.5+" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Administración de nodos basada en PKI, requiere versión de firmware 2.5+" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -26193,6 +30518,12 @@ }, "Please be advised that because the map report is not encrypted, your data may be stored and displayed permanently by third parties. Meshtastic does not assume responsibility for any such storage, display or disclosure of this data." : { "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Tenga en cuenta que debido a que el informe del mapa no está cifrado, terceros pueden almacenar y mostrar sus datos de forma permanente. Meshtastic no asume responsabilidad por dicho almacenamiento, exhibición o divulgación de estos datos." + } + }, "ja" : { "stringUnit" : { "state" : "translated", @@ -26221,6 +30552,12 @@ "value" : "Bitte verbinde dich mit einem Funkgerät, um die Einstellungen zu ändern." } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Conéctese a una radio para configurar los ajustes." + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -26261,6 +30598,12 @@ "value" : "Bitte lege eine Region fest" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Por favor establece una región" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -26289,6 +30632,12 @@ }, "Points of Interest" : { "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Puntos de interés" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -26323,6 +30672,12 @@ "value" : "Kacke" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Caca" + } + }, "fr" : { "stringUnit" : { "state" : "translated", @@ -26381,6 +30736,12 @@ }, "Position" : { "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Posición" + } + }, "he" : { "stringUnit" : { "state" : "translated", @@ -26433,6 +30794,12 @@ "value" : "Positionseinstellungen" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Configuración de posición" + } + }, "fr" : { "stringUnit" : { "state" : "translated", @@ -26497,6 +30864,12 @@ "value" : "Positionskonfiguration empfangen: %@" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Configuración de posición recibida: %@" + } + }, "fr" : { "stringUnit" : { "state" : "translated", @@ -26549,6 +30922,12 @@ }, "Position Exchange Failed" : { "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Error en el intercambio de posición" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -26577,6 +30956,12 @@ }, "Position Exchange Requested" : { "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Intercambio de posición solicitado" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -26605,6 +30990,12 @@ }, "Position Flags" : { "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Banderas de posición" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -26633,6 +31024,12 @@ }, "Position Log" : { "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Registro de posición" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -26667,6 +31064,12 @@ }, "Position Log %lld Points" : { "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Registro de posición %lld puntos" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -26695,6 +31098,12 @@ }, "Position Packet" : { "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Paquete de posición" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -26729,6 +31138,12 @@ "value" : "Position gesendet" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Posición enviada" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -26757,6 +31172,12 @@ }, "Positions Enabled" : { "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Posiciones Habilitadas" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -26791,6 +31212,12 @@ }, "Positions will be provided by your device GPS, if you select disabled or not present you can set a fixed position." : { "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Las posiciones serán proporcionadas por el GPS de su dispositivo; si selecciona deshabilitado o no presente, puede establecer una posición fija." + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -26831,6 +31258,12 @@ "value" : "Strom" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Fuerza" + } + }, "he" : { "stringUnit" : { "state" : "translated", @@ -26889,6 +31322,12 @@ "value" : "Stromkonfiguration" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Configuración de energía" + } + }, "he" : { "stringUnit" : { "state" : "translated", @@ -26941,6 +31380,12 @@ }, "Power config received: %@" : { "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Configuración de energía recibida: %@" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -26975,6 +31420,12 @@ }, "Power Metrics" : { "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Métricas de energía" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -27003,6 +31454,12 @@ }, "Power Metrics Log" : { "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Registro de métricas de energía" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -27037,6 +31494,12 @@ }, "Power Off" : { "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Apagar" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -27066,6 +31529,12 @@ "Power Options" : { "extractionState" : "stale", "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Opciones de energía" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -27100,6 +31569,12 @@ "value" : "Stromsparen" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ahorro de energía" + } + }, "he" : { "stringUnit" : { "state" : "translated", @@ -27152,6 +31627,12 @@ }, "Power Screen" : { "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Pantalla de energía" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -27179,7 +31660,14 @@ } }, "Power Sensor Options" : { - + "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Opciones de sensores de potencia" + } + } + } }, "Powered" : { "localizations" : { @@ -27189,6 +31677,12 @@ "value" : "Angeschaltet" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Motorizado" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -27223,6 +31717,12 @@ "value" : "Genaue Position" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ubicación precisa" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -27257,6 +31757,12 @@ "value" : "Voreinstellungen" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Preajustes" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -27291,6 +31797,12 @@ }, "Press Pin" : { "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Pin de prensa" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -27319,6 +31831,12 @@ }, "Pressure" : { "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Presión" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -27353,6 +31871,12 @@ "value" : "Primär" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Primario" + } + }, "fr" : { "stringUnit" : { "state" : "translated", @@ -27417,6 +31941,12 @@ "value" : "Erster Admin-Schlüssel" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Clave de administrador principal" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -27451,6 +31981,12 @@ }, "Primary GPIO" : { "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "GPIO primario" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -27485,6 +32021,12 @@ "value" : "Privater Schlüssel" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Clave privada" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -27525,6 +32067,12 @@ "value" : "Prozess" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Proceso" + } + }, "fr" : { "stringUnit" : { "state" : "translated", @@ -27589,6 +32137,12 @@ "value" : "Datei wird verarbeitet…" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Procesando archivo..." + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -27629,6 +32183,12 @@ "value" : "Projektinformationen" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Información del proyecto" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -27669,6 +32229,12 @@ "value" : "Protobufs" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Protobufs" + } + }, "fr" : { "stringUnit" : { "state" : "translated", @@ -27733,6 +32299,12 @@ "value" : "Teile anonyme Nutzungsstatistiken und Absturzberichte." } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Proporcione estadísticas de uso anónimas e informes de fallos." + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -27743,6 +32315,12 @@ }, "Provide Confirmation" : { "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Proporcionar confirmación" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -27759,6 +32337,12 @@ "value" : "Öffentlicher Schlüssel" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Clave pública" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -27793,6 +32377,12 @@ }, "Public Key Encryption" : { "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Cifrado de clave pública" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -27827,6 +32417,12 @@ }, "Public Key Mismatch" : { "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "La clave pública no coincide" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -27861,6 +32457,12 @@ }, "PWD" : { "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "PCD" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -27895,6 +32497,12 @@ "value" : "Fragezeichen" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Pregunta" + } + }, "fr" : { "stringUnit" : { "state" : "translated", @@ -27953,6 +32561,12 @@ }, "Radiation" : { "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Radiación" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -27987,6 +32601,12 @@ "value" : "Geräteeinstellungen" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Configuración de radio" + } + }, "fr" : { "stringUnit" : { "state" : "translated", @@ -28051,6 +32671,12 @@ "value" : "RAK Drehimpulsgeber Modul" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Codificador rotatorio RAK" + } + }, "fr" : { "stringUnit" : { "state" : "translated", @@ -28115,6 +32741,12 @@ "value" : "Entfernungstest" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Prueba de rango" + } + }, "fr" : { "stringUnit" : { "state" : "translated", @@ -28179,6 +32811,12 @@ "value" : "Entfernungstest Konfiguration" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Configuración de prueba de rango" + } + }, "fr" : { "stringUnit" : { "state" : "translated", @@ -28243,6 +32881,12 @@ "value" : "Range Test Modul konfiguration empfangen: %@" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Configuración del módulo de prueba de rango recibida: %@" + } + }, "fr" : { "stringUnit" : { "state" : "translated", @@ -28307,6 +32951,12 @@ "value" : "Neustart" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Reiniciar" + } + }, "fr" : { "stringUnit" : { "state" : "translated", @@ -28371,6 +33021,12 @@ "value" : "Knoten neustarten?" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "¿Reiniciar el nodo?" + } + }, "fr" : { "stringUnit" : { "state" : "translated", @@ -28429,6 +33085,12 @@ }, "Rebroadcast any observed message, if it was on our private channel or from another mesh with the same lora params." : { "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Retransmitir cualquier mensaje observado, si fue en nuestro canal privado o desde otra malla con los mismos parámetros de lora." + } + }, "ja" : { "stringUnit" : { "state" : "translated", @@ -28451,6 +33113,12 @@ }, "Rebroadcast Mode" : { "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Modo de retransmisión" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -28485,6 +33153,12 @@ }, "Receive data (rxd) GPIO pin" : { "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Recibir datos (rxd) pin GPIO" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -28519,6 +33193,12 @@ "value" : "Negative Empfangsbestätigung empfangen" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Recibí un reconocimiento negativo." + } + }, "fr" : { "stringUnit" : { "state" : "translated", @@ -28584,6 +33264,12 @@ "value" : "Empfangsbestätigung" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Confirmación recibida" + } + }, "fr" : { "stringUnit" : { "state" : "translated", @@ -28652,6 +33338,12 @@ "value" : "Recipient Ack" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Confirmación del destinatario" + } + }, "fr" : { "stringUnit" : { "state" : "translated", @@ -28719,6 +33411,12 @@ "value" : "Route aufzeichnen" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ruta de grabación" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -28747,6 +33445,12 @@ }, "Refresh device metadata" : { "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Actualizar metadatos del dispositivo" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -28775,6 +33479,12 @@ }, "Regenerate Private Key" : { "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Regenerar clave privada" + } + }, "ja" : { "stringUnit" : { "state" : "translated", @@ -28797,6 +33507,12 @@ "value" : "Region" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Región" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -28837,6 +33553,12 @@ "value" : "Regionale Einschaltdauergrenze erreicht" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Se alcanzó el límite del ciclo de trabajo regional" + } + }, "fr" : { "stringUnit" : { "state" : "translated", @@ -28905,6 +33627,12 @@ }, "Release Notes" : { "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Notas de la versión" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -28933,6 +33661,12 @@ }, "Remote administration for: %@" : { "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Administración remota para: %@" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -28961,6 +33695,12 @@ }, "Remote Legacy Admin: %@" : { "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Administrador remoto heredado: %@" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -28989,6 +33729,12 @@ }, "Remote PKI Admin: %@" : { "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Administrador de PKI remoto: %@" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -29023,6 +33769,12 @@ "value" : "Entfernen" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Eliminar" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -29057,6 +33809,12 @@ "value" : "Von Favoriten entfernen" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Quitar de favoritos" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -29085,6 +33843,12 @@ }, "Remove from ignored" : { "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Quitar de ignorado" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -29125,6 +33889,12 @@ "value" : "Repeater" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Reloj de repetición" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -29159,6 +33929,12 @@ }, "Replace Channels" : { "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Reemplazar canales" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -29193,6 +33969,12 @@ "value" : "Antworten" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Responder" + } + }, "fr" : { "stringUnit" : { "state" : "translated", @@ -29251,6 +34033,12 @@ }, "Request Legacy Admin: %@" : { "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Solicitar administrador heredado: %@" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -29279,6 +34067,12 @@ }, "Request PKI Admin: %@" : { "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Solicitar administrador de PKI: %@" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -29307,6 +34101,12 @@ }, "Requested Canned Messages Module Messages for node: %@" : { "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Mensajes del módulo de mensajes predefinidos solicitados para el nodo: %@" + } + }, "fr" : { "stringUnit" : { "state" : "translated", @@ -29365,6 +34165,12 @@ }, "Requires that there be an accelerometer on your device." : { "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Requiere que haya un acelerómetro en su dispositivo." + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -29399,6 +34205,12 @@ "value" : "App-Einstellungen zurücksetzen" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Restablecer la configuración de la aplicación" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -29439,6 +34251,12 @@ "value" : "Knotendatenbank zurücksetzen" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Restablecer NodeDB" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -29473,6 +34291,12 @@ "value" : "Neustarten" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Reanudar" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -29507,6 +34331,12 @@ "value" : "Verbundenen Knoten neustarten" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Reinicie en el nodo al que está conectado" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -29535,6 +34365,12 @@ }, "Restore" : { "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Restaurar" + } + }, "ja" : { "stringUnit" : { "state" : "translated", @@ -29557,6 +34393,12 @@ "value" : "Fortsetzen" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Reanudar" + } + }, "fr" : { "stringUnit" : { "state" : "translated", @@ -29615,6 +34457,12 @@ }, "Retreiving nodes . ." : { "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Recuperando nodos. ." + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -29625,6 +34473,12 @@ }, "Retreiving nodes %lld" : { "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Recuperando nodos %lld" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -29635,6 +34489,12 @@ }, "Retrieving nodes" : { "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Recuperando nodos" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -29645,6 +34505,12 @@ }, "Retrieving nodes %lld" : { "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Recuperando nodos %lld" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -29655,6 +34521,12 @@ }, "Retrying (attempt %lld)" : { "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Reintentando (intento %lld)" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -29671,6 +34543,12 @@ "value" : "App bewerten" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Revisa la aplicación" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -29711,6 +34589,12 @@ "value" : "Rechts" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Bien" + } + }, "fr" : { "stringUnit" : { "state" : "translated", @@ -29775,6 +34659,12 @@ "value" : "Klingelton" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Tono de llamada" + } + }, "fr" : { "stringUnit" : { "state" : "translated", @@ -29839,6 +34729,12 @@ "value" : "Klingelton Konfiguration" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Configuración de tono de llamada" + } + }, "he" : { "stringUnit" : { "state" : "translated", @@ -29891,6 +34787,12 @@ }, "Ringtone Transfer Language" : { "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Idioma de transferencia de tono de llamada" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -29931,6 +34833,12 @@ }, "Ringtone Transfer Language(RTTTL) Ringtone String used by supported buzzers in external notifications." : { "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Lenguaje de transferencia de tono de llamada (RTTTL) Cadena de tono utilizada por los timbres compatibles en notificaciones externas." + } + }, "he" : { "stringUnit" : { "state" : "translated", @@ -29989,6 +34897,12 @@ "value" : "Rolle" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Role" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -30029,6 +34943,12 @@ "value" : "Rolle: %@" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Role: %@" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -30063,6 +34983,12 @@ "value" : "Rollen" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Roles" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -30091,6 +35017,12 @@ }, "Root Topic" : { "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Tema raíz" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -30119,6 +35051,12 @@ }, "Rotary 1" : { "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Giratorio 1" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -30153,6 +35091,12 @@ }, "Route Back: %@" : { "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ruta de regreso: %@" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -30181,6 +35125,12 @@ }, "Route Lines" : { "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Líneas de ruta" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -30215,6 +35165,12 @@ "value" : "Routenliste" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Lista de rutas" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -30231,6 +35187,12 @@ "value" : "Route aufzeichnen" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Grabador de ruta" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -30271,6 +35233,12 @@ "value" : "Routenaufzeichnung pausiert" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Grabación de ruta en pausa" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -30305,6 +35273,12 @@ "value" : "Route: %@" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ruta: %@" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -30339,6 +35313,12 @@ "value" : "Router" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Enrutador" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -30379,6 +35359,12 @@ "value" : "Router mit Verzögerung" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Enrutador tarde" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -30413,6 +35399,12 @@ "value" : "Routenliste" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Rutas" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -30453,6 +35445,12 @@ "value" : "Routing empfangen für RequestID: %@ Ack Status: %@" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Enrutamiento recibido para RequestID: %@ Estado de confirmación: %@" + } + }, "fr" : { "stringUnit" : { "state" : "translated", @@ -30511,6 +35509,12 @@ }, "RSSI %@ dBm" : { "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "RSSI %@dBm" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -30539,6 +35543,12 @@ }, "RSSI %ddB" : { "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "RSSI %ddB" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -30567,6 +35577,12 @@ }, "RSSI %llddB" : { "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "RSSI %llddB" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -30601,6 +35617,12 @@ "value" : "RTTTL Klingeltonkonfiguration empfangen: %@" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Configuración de tono de llamada RTTTL recibida: %@" + } + }, "fr" : { "stringUnit" : { "state" : "translated", @@ -30659,6 +35681,12 @@ }, "Russia" : { "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Rusia" + } + }, "ja" : { "stringUnit" : { "state" : "translated", @@ -30681,6 +35709,12 @@ }, "RX Boosted Gain" : { "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ganancia impulsada por RX" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -30709,6 +35743,12 @@ }, "Same as behavior as ALL but skips packet decoding and simply rebroadcasts them. Only available in Repeater role. Setting this on any other roles will result in ALL behavior." : { "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Igual que el comportamiento de ALL, pero omite la decodificación de paquetes y simplemente los retransmite. Sólo disponible en rol de Repetidor. Establecer esto en cualquier otro rol dará como resultado TODOS los comportamientos." + } + }, "ja" : { "stringUnit" : { "state" : "translated", @@ -30737,6 +35777,12 @@ "value" : "Satellit" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Satélite" + } + }, "he" : { "stringUnit" : { "state" : "translated", @@ -30783,6 +35829,12 @@ }, "Satellite Flyover" : { "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Sobrevuelo satelital" + } + }, "fr" : { "stringUnit" : { "state" : "translated", @@ -30847,6 +35899,12 @@ "value" : "Satelliten" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "sábados" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -30881,6 +35939,12 @@ "value" : "Satelliten Schätzung %lld" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Estimación de sats %lld" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -30915,6 +35979,12 @@ "value" : "Satelliten in Sicht: %@" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Sats a la vista: %@" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -30949,6 +36019,12 @@ "value" : "Speichern" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ahorrar" + } + }, "fr" : { "stringUnit" : { "state" : "translated", @@ -31007,6 +36083,12 @@ }, "Save Channel Settings" : { "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Guardar configuración del canal" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -31041,6 +36123,12 @@ "value" : "Speichere Konfiguration für %@" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Guardar configuración para %@" + } + }, "fr" : { "stringUnit" : { "state" : "translated", @@ -31105,6 +36193,12 @@ "value" : "Benutzerkonfiguration nach %@ speichern?" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "¿Guardar configuración de usuario en %@?" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -31139,6 +36233,12 @@ }, "Saves a CSV with the range test message details, currently only available on ESP32 devices with a web server." : { "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Guarda un CSV con los detalles del mensaje de prueba de rango, actualmente solo disponible en dispositivos ESP32 con un servidor web." + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -31173,6 +36273,12 @@ }, "Scan this QR code to add %@ to another device." : { "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Escanee este código QR para agregar %@ a otro dispositivo." + } + }, "ja" : { "stringUnit" : { "state" : "translated", @@ -31195,6 +36301,12 @@ }, "Screen on for" : { "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Pantalla encendida para" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -31229,6 +36341,12 @@ "value" : "Suchen" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Buscar" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -31257,6 +36375,12 @@ }, "Second" : { "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Segundo" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -31291,6 +36415,12 @@ "value" : "Sekundär" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Secundario" + } + }, "fr" : { "stringUnit" : { "state" : "translated", @@ -31355,6 +36485,12 @@ "value" : "Zweiter Admin-Schlüssel" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Clave de administrador secundario" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -31395,6 +36531,12 @@ "value" : "Sicherheit" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Seguridad" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -31435,6 +36577,12 @@ "value" : "Sicherheitskonfiguration" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Configuración de seguridad" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -31475,6 +36623,12 @@ "value" : "Sicherheitskonfigurationseinstellungen erfordern eine Firmware mit Version 2.5 oder höher" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Los ajustes de configuración de seguridad requieren una versión de firmware 2.5+" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -31515,6 +36669,12 @@ "value" : "Auswählen" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Seleccionar" + } + }, "fr" : { "stringUnit" : { "state" : "translated", @@ -31579,6 +36739,12 @@ "value" : "Kanal wählen" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Seleccione un canal" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -31607,6 +36773,12 @@ }, "Select a conversation" : { "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Seleccione una conversación" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -31635,6 +36807,12 @@ }, "Select a conversation type" : { "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Seleccione un tipo de conversación" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -31668,10 +36846,23 @@ } }, "Select a Node" : { - + "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Seleccione un nodo" + } + } + } }, "Select a node from the drop down to manage connected or remote devices." : { "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Seleccione un nodo del menú desplegable para administrar dispositivos conectados o remotos." + } + }, "ja" : { "stringUnit" : { "state" : "translated", @@ -31694,6 +36885,12 @@ }, "Select a Trace Route" : { "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Seleccione una ruta de seguimiento" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -31722,6 +36919,12 @@ }, "Select Channel" : { "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Seleccionar canal" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -31756,6 +36959,12 @@ "value" : "Datei auswählen" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Seleccionar archivo de mapa" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -31772,6 +36981,12 @@ "value" : "Als kritisch eingestufte Mitteilungen ignorieren den Stummschalter und die 'Nicht stören'-Einstellungen des Benachrichtigungszentrums." } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Los paquetes seleccionados enviados como críticos ignorarán el interruptor de silencio y la configuración de No molestar en el centro de notificaciones del sistema operativo." + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -31788,6 +37003,12 @@ "value" : "Senden" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Enviar" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -31822,6 +37043,12 @@ "value" : "Sende ${messageContent} an ${channelNumber}" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Enviar ${messageContent} a ${channelNumber}" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -31850,6 +37077,12 @@ }, "Send ${messageContent} to ${nodeNumber}" : { "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Enviar ${messageContent} a ${nodeNumber}" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -31878,6 +37111,12 @@ }, "Send a Direct Message" : { "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Enviar un mensaje directo" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -31894,6 +37133,12 @@ "value" : "Gruppennachricht senden" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Enviar un mensaje grupal" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -31922,6 +37167,12 @@ }, "Send a heartbeat to advertise the server's presence." : { "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Envíe un latido para anunciar la presencia del servidor." + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -31950,6 +37201,12 @@ }, "Send a message to a certain meshtastic channel" : { "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Enviar un mensaje a un determinado canal meshtastic" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -31978,6 +37235,12 @@ }, "Send a message to a certain meshtastic node" : { "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Enviar un mensaje a un determinado nodo meshtastic" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -31988,6 +37251,12 @@ }, "Send a position on the primary channel when the user button is triple clicked." : { "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Envíe una posición en el canal principal cuando se haga triple clic en el botón del usuario." + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -32028,6 +37297,12 @@ "value" : "Herunterfahren an verbundenen Knoten senden" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Envía un apagado al nodo al que estás conectado" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -32062,6 +37337,12 @@ "value" : "Wegpunkt senden" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Enviar un punto de ruta" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -32090,6 +37371,12 @@ }, "Send ASCII bell with alert message. Useful for triggering external notification on bell." : { "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Enviar campana ASCII con mensaje de alerta. Útil para activar notificaciones externas al tocar el timbre." + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -32130,6 +37417,12 @@ "value" : "Sende Glocke" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Enviar campana" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -32170,6 +37463,12 @@ "value" : "Herzschlag senden" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Enviar latido" + } + }, "fr" : { "stringUnit" : { "state" : "translated", @@ -32234,6 +37533,12 @@ "value" : "Mitteilungen senden" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Enviar notificaciones" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -32244,6 +37549,12 @@ }, "Send Reboot OTA" : { "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Enviar Reiniciar OTA" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -32278,6 +37589,12 @@ }, "Sender Interval" : { "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Intervalo del remitente" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -32318,6 +37635,12 @@ "value" : "Sensor" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Sensor" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -32352,6 +37675,12 @@ }, "Sensor options" : { "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Opciones de sensores" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -32381,6 +37710,12 @@ "Sensor Options" : { "extractionState" : "stale", "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Opciones de sensores" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -32409,6 +37744,12 @@ }, "Sent a Channel for: %@ Channel Index %d" : { "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Envió un canal para: %@ Índice de canales %d" + } + }, "fr" : { "stringUnit" : { "state" : "translated", @@ -32473,6 +37814,12 @@ "value" : "LoRa.Config gesendet für: %@" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Envió un LoRa.Config para: %@" + } + }, "fr" : { "stringUnit" : { "state" : "translated", @@ -32538,6 +37885,12 @@ "value" : "Position von Apple Gerät an Knoten gesendet: %@" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Envió un paquete de posición desde el GPS del dispositivo Apple al nodo: %@@" + } + }, "fr" : { "stringUnit" : { "state" : "translated", @@ -32602,6 +37955,12 @@ "value" : "Sende Traceroute Anforderung zu Knoten: %@" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Envió una solicitud de ruta de seguimiento al nodo: %@" + } + }, "fr" : { "stringUnit" : { "state" : "translated", @@ -32666,6 +38025,12 @@ "value" : "Wegpunkt gesendet von: %@" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Enviado un paquete de waypoint desde: %@" + } + }, "fr" : { "stringUnit" : { "state" : "translated", @@ -32730,6 +38095,12 @@ "value" : "Sende Nachricht %@ von %@ an %@" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Mensaje enviado %@ de %@ a %@" + } + }, "fr" : { "stringUnit" : { "state" : "translated", @@ -32794,6 +38165,12 @@ "value" : "Sequenznummer" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Número de secuencia" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -32828,6 +38205,12 @@ "value" : "Sequenz: %@" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Secuencia: %@" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -32856,6 +38239,12 @@ }, "Serial" : { "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "De serie" + } + }, "fr" : { "stringUnit" : { "state" : "translated", @@ -32920,6 +38309,12 @@ "value" : "Serial Konfiguration" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Configuración en serie" + } + }, "fr" : { "stringUnit" : { "state" : "translated", @@ -32984,6 +38379,12 @@ "value" : "Serielle Konsole" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Consola serie" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -33018,6 +38419,12 @@ "value" : "Serielle Konsole über die Stream-API." } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Consola serial a través de Stream API." + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -33052,6 +38459,12 @@ "value" : "Serial Modul Konfiguration empfangen: %@" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Configuración del módulo serie recibida: %@" + } + }, "fr" : { "stringUnit" : { "state" : "translated", @@ -33110,6 +38523,12 @@ }, "Series" : { "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Serie" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -33144,6 +38563,12 @@ "value" : "Server" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Servidor" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -33178,6 +38603,12 @@ "value" : "Serveradresse" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Dirección del servidor" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -33206,6 +38637,12 @@ }, "Server Option" : { "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Opción de servidor" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -33234,6 +38671,12 @@ }, "Set" : { "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Colocar" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -33268,6 +38711,12 @@ "value" : "Setze LoRa Region" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Establecer región LoRa" + } + }, "fr" : { "stringUnit" : { "state" : "translated", @@ -33326,6 +38775,12 @@ }, "Set the GPIO pins for RXD and TXD." : { "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Configure los pines GPIO para RXD y TXD." + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -33354,6 +38809,12 @@ }, "Set to current location" : { "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Establecer en la ubicación actual" + } + }, "ja" : { "stringUnit" : { "state" : "translated", @@ -33370,6 +38831,12 @@ }, "Sets the maximum number of hops, default is 3. Increasing hops also increases congestion and should be used carefully. O hop broadcast messages will not get ACKs." : { "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Establece el número máximo de saltos; el valor predeterminado es 3. El aumento de saltos también aumenta la congestión y debe usarse con cuidado. Los mensajes de difusión de O hop no recibirán ACK." + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -33398,6 +38865,12 @@ }, "Sets the screen clock format to 12-hour." : { "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Establece el formato del reloj de la pantalla en 12 horas." + } + }, "ja" : { "stringUnit" : { "state" : "translated", @@ -33420,6 +38893,12 @@ "value" : "Einstellungen" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "ajustes" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -33436,6 +38915,12 @@ "value" : "Einstellungen" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ajustes" + } + }, "fr" : { "stringUnit" : { "state" : "translated", @@ -33500,6 +38985,12 @@ "value" : "Zweiundsiebzig Stunden" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Setenta y dos horas" + } + }, "fr" : { "stringUnit" : { "state" : "translated", @@ -33558,6 +39049,12 @@ }, "Share Contact QR" : { "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Compartir Contacto QR" + } + }, "ja" : { "stringUnit" : { "state" : "translated", @@ -33586,6 +39083,12 @@ "value" : "Standort teilen" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Compartir ubicación" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -33602,6 +39105,12 @@ "value" : "Kanal QR Code teilen" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Compartir código QR" + } + }, "fr" : { "stringUnit" : { "state" : "translated", @@ -33666,6 +39175,12 @@ "value" : "QR Code & Link teilen" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Compartir código QR y enlace" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -33700,6 +39215,12 @@ "value" : "Teile deinen Standort in Echtzeit und koordiniere deine Gruppe mithilfe integrierter GPS-Funktionen." } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Comparta su ubicación en tiempo real y mantenga a su grupo coordinado con funciones de GPS integradas." + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -33716,6 +39237,12 @@ "value" : "Gemeinsamer Schlüssel" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Clave compartida" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -33750,6 +39277,12 @@ "value" : "Meshtastic Kanäle teilen" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Compartir canales Meshtastic" + } + }, "fr" : { "stringUnit" : { "state" : "translated", @@ -33814,6 +39347,12 @@ "value" : "Kurzname" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Nombre corto" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -33848,6 +39387,12 @@ }, "Short Range - Fast" : { "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Corto alcance - Rápido" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -33876,6 +39421,12 @@ }, "Short Range - Slow" : { "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Corto alcance - Lento" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -33904,6 +39455,12 @@ }, "Short Range - Turbo" : { "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Corto alcance - Turbo" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -33932,6 +39489,12 @@ }, "Show a confirmation dialog before performing the factory reset" : { "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Mostrar un cuadro de diálogo de confirmación antes de realizar el restablecimiento de fábrica" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -33948,6 +39511,12 @@ "value" : "Zeige Alarme" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Mostrar alertas" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -33982,6 +39551,12 @@ "value" : "Zeige Alarme" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Mostrar alertas" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -34016,6 +39591,12 @@ "value" : "Zeige Knoten" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Mostrar nodos" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -34050,6 +39631,12 @@ "value" : "Zeige auf dem Gerätebildschirm" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Mostrar en la pantalla del dispositivo" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -34084,6 +39671,12 @@ "value" : "Zeige auf der Netzwerkkarte." } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Mostrar en el mapa de malla." + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -34119,6 +39712,12 @@ "value" : "Zeige Wegpunkte" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Mostrar puntos de ruta" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -34147,6 +39746,12 @@ }, "Shows information for the connected Lora radio. You can swipe left to disconnect the radio and long press to start the live activity." : { "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Muestra información de la radio Lora conectada. Puede deslizar hacia la izquierda para desconectar la radio y mantener presionada para iniciar la actividad en vivo." + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -34163,6 +39768,12 @@ "value" : "Herunterfahren" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Cerrar" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -34197,6 +39808,12 @@ "value" : "Knoten herunterfahren?" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "¿Cerrar el nodo?" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -34231,6 +39848,12 @@ "value" : "Knoten herunterfahren?" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "¿Apagar el nodo?" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -34265,6 +39888,12 @@ "value" : "Herunterfahren bei Stromunterbruch" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Apagado por pérdida de energía" + } + }, "he" : { "stringUnit" : { "state" : "translated", @@ -34317,6 +39946,12 @@ }, "Signal %@" : { "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Señal %@" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -34351,6 +39986,12 @@ "value" : "Einfach" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Simple" + } + }, "fr" : { "stringUnit" : { "state" : "translated", @@ -34409,6 +40050,12 @@ }, "Singapore 923MHz" : { "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Singapur 923MHz" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -34443,6 +40090,12 @@ "value" : "Sechs Stunden" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Seis horas" + } + }, "fr" : { "stringUnit" : { "state" : "translated", @@ -34507,6 +40160,12 @@ "value" : "Skifahren" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Esquiar" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -34541,6 +40200,12 @@ }, "Smart Position" : { "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Posición inteligente" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -34569,6 +40234,12 @@ }, "SNR" : { "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "SNR" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -34597,6 +40268,12 @@ }, "SNR %@ dB" : { "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "SNR %@dB" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -34625,6 +40302,12 @@ }, "SNR %@dB" : { "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "SNR %@dB" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -34653,6 +40336,12 @@ }, "Soil Moisture" : { "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Humedad del suelo" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -34681,6 +40370,12 @@ }, "Soil Temp" : { "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Temperatura del suelo" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -34710,6 +40405,12 @@ "Specifies how long the monitored GPIO should output." : { "extractionState" : "stale", "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Especifica cuánto tiempo debe emitir el GPIO monitoreado." + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -34744,6 +40445,12 @@ "value" : "Geschwindigkeit" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Velocidad" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -34778,6 +40485,12 @@ "value" : "Geschwindigkeit %@" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Velocidad %@" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -34812,6 +40525,12 @@ "value" : "Geschwindigkeit: %@" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Velocidad: %@" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -34846,6 +40565,12 @@ "value" : "App-Entwicklung unterstützen" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Desarrollo de aplicaciones para patrocinadores" + } + }, "ja" : { "stringUnit" : { "state" : "translated", @@ -34862,6 +40587,12 @@ }, "Spread Factor" : { "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Factor de dispersión" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -34896,6 +40627,12 @@ "value" : "SSID" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "SSID" + } + }, "fr" : { "stringUnit" : { "state" : "translated", @@ -34954,6 +40691,12 @@ }, "Standard" : { "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Estándar" + } + }, "he" : { "stringUnit" : { "state" : "translated", @@ -35000,6 +40743,12 @@ }, "Standard Muted" : { "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Estándar silenciado" + } + }, "fr" : { "stringUnit" : { "state" : "translated", @@ -35064,6 +40813,12 @@ "value" : "Start" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Comenzar" + } + }, "fr" : { "stringUnit" : { "state" : "translated", @@ -35122,6 +40877,12 @@ }, "State Broadcast Interval" : { "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Intervalo de transmisión estatal" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -35156,6 +40917,12 @@ "value" : "Überall in Verbindung bleiben" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Manténgase conectado en cualquier lugar" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -35166,6 +40933,12 @@ }, "Store & Forward" : { "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Almacenar y reenviar" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -35200,6 +40973,12 @@ }, "Store & Forward Config" : { "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Almacenar y reenviar configuración" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -35234,6 +41013,12 @@ }, "Store & Forward module config received: %@" : { "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Configuración del módulo Store & Forward recibida: %@" + } + }, "fr" : { "stringUnit" : { "state" : "translated", @@ -35292,6 +41077,12 @@ }, "Store and forward servers require an ESP32 device with PSRAM or Linux Native." : { "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Los servidores de almacenamiento y reenvío requieren un dispositivo ESP32 con PSRAM o Linux Native." + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -35320,6 +41111,12 @@ }, "Subscribed" : { "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "suscrito" + } + }, "he" : { "stringUnit" : { "state" : "translated", @@ -35354,6 +41151,12 @@ }, "Subsystem" : { "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "subsistema" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -35394,6 +41197,12 @@ "value" : "Successfully uploaded '%1$@' with %2$lld overlays" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "'%@' subido correctamente con %lld superposiciones" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -35410,6 +41219,12 @@ "value" : "Unterstützt" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Apoyado" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -35438,6 +41253,12 @@ }, "Supported I2C Connected sensors will be detected automatically, sensors are BMP280, BME280, BME680, MCP9808, INA219, INA260, LPS22 and SHTC3." : { "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Los sensores conectados I2C compatibles se detectarán automáticamente, los sensores son BMP280, BME280, BME680, MCP9808, INA219, INA260, LPS22 y SHTC3." + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -35472,6 +41293,12 @@ }, "Table" : { "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Mesa" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -35500,6 +41327,12 @@ }, "Taiwan" : { "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Taiwán" + } + }, "ja" : { "stringUnit" : { "state" : "translated", @@ -35528,6 +41361,12 @@ "value" : "TAK" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "NO" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -35568,6 +41407,12 @@ "value" : "TAK Tracker" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "SÍ rastreador" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -35602,6 +41447,12 @@ }, "Takes a Meshtastic channel URL and saves the channel settings." : { "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Toma la URL de un canal Meshtastic y guarda la configuración del canal." + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -35630,6 +41481,12 @@ }, "Takes a Meshtastic contact URL and saves it to the nodes database" : { "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Toma una URL de contacto Meshtastic y la guarda en la base de datos de nodos" + } + }, "ja" : { "stringUnit" : { "state" : "translated", @@ -35658,6 +41515,12 @@ "value" : "Tapback Antwort" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Tapback" + } + }, "fr" : { "stringUnit" : { "state" : "translated", @@ -35715,6 +41578,14 @@ } }, "TCP" : { + "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "tcp" + } + } + }, "shouldTranslate" : false }, "Telemetry" : { @@ -35725,6 +41596,12 @@ "value" : "Telemetrie (Sensoren)" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Telemetria" + } + }, "fr" : { "stringUnit" : { "state" : "translated", @@ -35789,6 +41666,12 @@ "value" : "Telemetrie Einstellungen" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Configuración de telemetría" + } + }, "fr" : { "stringUnit" : { "state" : "translated", @@ -35853,6 +41736,12 @@ "value" : "Telemetrie Modul Konfiguration empfangen: %@" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Configuración del módulo de telemetría recibida: %@" + } + }, "fr" : { "stringUnit" : { "state" : "translated", @@ -35917,6 +41806,12 @@ "value" : "Temp" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Temperatura" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -35951,6 +41846,12 @@ "value" : "Temperatur" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Temperatura" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -35985,6 +41886,12 @@ "value" : "Zehn Minuten" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "diez minutos" + } + }, "fr" : { "stringUnit" : { "state" : "translated", @@ -36049,6 +41956,12 @@ "value" : "Zehn Sekunden" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Diez segundos" + } + }, "fr" : { "stringUnit" : { "state" : "translated", @@ -36113,6 +42026,12 @@ "value" : "Dritter Admin-Schlüssel" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Clave de administrador terciario" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -36153,6 +42072,12 @@ "value" : "Textnachricht" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Mensaje de texto" + } + }, "fr" : { "stringUnit" : { "state" : "translated", @@ -36211,6 +42136,12 @@ }, "TFT Full Color Displays" : { "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Pantallas TFT a todo color" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -36239,6 +42170,12 @@ }, "Thailand" : { "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Tailandia" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -36267,6 +42204,12 @@ }, "The amount of time to wait before we consider your packet as done." : { "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "La cantidad de tiempo que debemos esperar antes de que consideremos que su paquete está listo." + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -36295,6 +42238,12 @@ }, "The compass heading on the screen outside of the circle will always point north." : { "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "El rumbo de la brújula en la pantalla fuera del círculo siempre apuntará al norte." + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -36329,6 +42278,12 @@ "value" : "Der Taupunkt ist gerade %@" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "El punto de rocío es %@ en este momento." + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -36357,6 +42312,12 @@ }, "The fastest that position updates will be sent if the minimum distance has been satisfied" : { "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Lo más rápido que se enviarán las actualizaciones de posición si se ha cumplido la distancia mínima" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -36391,6 +42352,12 @@ "value" : "Die letzten 4 Zeichen der MAC-Adresse des Geräts werden an den Kurznamen angehängt, um den BLE-Namen des Geräts festzulegen. Der Kurzname kann bis zu 4 Byte lang sein." } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Los últimos 4 de la dirección MAC del dispositivo se agregarán al nombre corto para configurar el nombre BLE del dispositivo. El nombre corto puede tener hasta 4 bytes de longitud." + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -36425,6 +42392,12 @@ }, "The maximum interval that can elapse without a node broadcasting a position" : { "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "El intervalo máximo que puede transcurrir sin que un nodo transmita una posición." + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -36453,6 +42426,12 @@ }, "The Meshtastic Apple apps support firmware version %@ and above." : { "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Las aplicaciones Meshtastic de Apple admiten la versión de firmware %@ y superior." + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -36487,6 +42466,12 @@ }, "The minimum distance change in meters to be considered for a smart position broadcast." : { "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "El cambio mínimo de distancia en metros a considerar para una transmisión de posición inteligente." + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -36527,6 +42512,12 @@ "value" : "Das Paket ist zu groß" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "El paquete es demasiado grande." + } + }, "fr" : { "stringUnit" : { "state" : "translated", @@ -36591,6 +42582,12 @@ "value" : "Der erste öffentliche Schlüssel, der berechtigt ist, Admin-Nachrichten an diesen Knoten zu senden." } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "La clave pública principal autorizada para enviar mensajes de administrador a este nodo." + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -36631,6 +42628,12 @@ "value" : "Die Region, in der du deine Funkgeräte verwenden wirst." } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "La región donde utilizará sus radios." + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -36665,6 +42668,12 @@ }, "The root topic to use for MQTT." : { "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "El tema raíz que se utilizará para MQTT." + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -36699,6 +42708,12 @@ }, "The Router roles are only for high vantage locations like mountaintops and towers with few nearby nodes, not for use in urban areas. Improper use will hurt your local mesh." : { "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Las funciones de enrutador son solo para ubicaciones estratégicas, como cimas de montañas y torres con pocos nodos cercanos, no para uso en áreas urbanas. El uso inadecuado dañará su malla local." + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -36715,6 +42730,12 @@ "value" : "Der zweite öffentliche Schlüssel, der berechtigt ist, Admin-Nachrichten an diesen Knoten zu senden." } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "La clave pública secundaria autorizada para enviar mensajes de administrador a este nodo." + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -36755,6 +42776,12 @@ "value" : "Status der LED (an/aus)" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "El estado del LED (encendido/apagado)" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -36789,6 +42816,12 @@ "value" : "Der dritte öffentliche Schlüssel, der berechtigt ist, Admin-Nachrichten an diesen Knoten zu senden." } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "La clave pública terciaria autorizada para enviar mensajes de administrador a este nodo." + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -36823,6 +42856,12 @@ }, "The URL for the channel settings" : { "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "La URL para la configuración del canal." + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -36851,6 +42890,12 @@ }, "The URL for the node to add" : { "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "La URL del nodo a agregar." + } + }, "ja" : { "stringUnit" : { "state" : "translated", @@ -36873,6 +42918,12 @@ }, "There has been no response to a request for device metadata via PKC admin for this node." : { "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "No ha habido respuesta a una solicitud de metadatos del dispositivo a través del administrador de PKC para este nodo." + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -36883,6 +42934,12 @@ }, "There is an issue with this contact's public key." : { "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Hay un problema con la clave pública de este contacto." + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -36898,6 +42955,12 @@ "These settings will %@ channels. The current LoRa Config will be replaced, if there are substantial changes to the LoRa config the device will reboot" : { "extractionState" : "stale", "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Estas configuraciones %@ canales. La configuración LoRa actual será reemplazada; si hay cambios sustanciales en la configuración LoRa, el dispositivo se reiniciará" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -36932,6 +42995,12 @@ "value" : "Dreißig Minuten" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "treinta minutos" + } + }, "fr" : { "stringUnit" : { "state" : "translated", @@ -36996,6 +43065,12 @@ "value" : "Dreißig Sekunden" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "treinta segundos" + } + }, "fr" : { "stringUnit" : { "state" : "translated", @@ -37060,6 +43135,12 @@ "value" : "Sechsunddreissig Stunden" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "treinta y seis horas" + } + }, "fr" : { "stringUnit" : { "state" : "translated", @@ -37118,6 +43199,12 @@ }, "This conversation will be deleted." : { "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Esta conversación será eliminada." + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -37146,6 +43233,12 @@ }, "This could take a while, response will appear in the trace route log for the node it was sent to." : { "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Esto podría tardar un poco; la respuesta aparecerá en el registro de ruta de seguimiento del nodo al que se envió." + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -37174,6 +43267,12 @@ }, "This device will send out range test messages on the selected interval." : { "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Este dispositivo enviará mensajes de prueba de alcance en el intervalo seleccionado." + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -37214,6 +43313,12 @@ "value" : "Diese Nachricht wurde höchstwahrscheinlich nicht übermittelt." } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Es probable que este mensaje no se haya entregado." + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -37242,6 +43347,12 @@ }, "This node does not support any configurable modules." : { "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Este nodo no admite ningún módulo configurable." + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -37270,6 +43381,12 @@ }, "This will disable fixed position and remove the currently set position." : { "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Esto desactivará la posición fija y eliminará la posición establecida actualmente." + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -37298,6 +43415,12 @@ }, "This will send a current position from your phone and enable fixed position." : { "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Esto enviará una posición actual desde su teléfono y habilitará la posición fija." + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -37338,6 +43461,12 @@ "value" : "Drei Stunden" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "tres horas" + } + }, "fr" : { "stringUnit" : { "state" : "translated", @@ -37402,6 +43531,12 @@ "value" : "Drei Sekunden" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "tres segundos" + } + }, "fr" : { "stringUnit" : { "state" : "translated", @@ -37466,6 +43601,12 @@ "value" : "Daumen runter" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Pulgar hacia abajo" + } + }, "fr" : { "stringUnit" : { "state" : "translated", @@ -37530,6 +43671,12 @@ "value" : "Daumen hoch" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Pulgares hacia arriba" + } + }, "fr" : { "stringUnit" : { "state" : "translated", @@ -37594,6 +43741,12 @@ "value" : "Zeit" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Tiempo" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -37628,6 +43781,12 @@ "value" : "Zeitstempel" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Marca de tiempo" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -37662,6 +43821,12 @@ "value" : "Zeitzone" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Huso horario" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -37696,6 +43861,12 @@ "value" : "Zeitzone für Daten auf dem Gerätebildschirm und Log." } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Zona horaria para fechas en la pantalla del dispositivo y registro." + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -37730,6 +43901,12 @@ "value" : "Zeitlimit erreicht" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Se acabó el tiempo" + } + }, "fr" : { "stringUnit" : { "state" : "translated", @@ -37794,6 +43971,12 @@ "value" : "Zeitstempel" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Marca de tiempo" + } + }, "fr" : { "stringUnit" : { "state" : "translated", @@ -37852,6 +44035,12 @@ }, "Timing and Overrides" : { "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Temporización y anulaciones" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -37862,6 +44051,12 @@ }, "TLS Enabled" : { "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "TLS habilitado" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -37896,6 +44091,12 @@ }, "To comply with privacy laws like CCPA and GDPR, we avoid sharing exact location data. Instead, we use anonymized or approximate (imprecise) location information to protect your privacy." : { "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Para cumplir con las leyes de privacidad como CCPA y GDPR, evitamos compartir datos de ubicación exacta. En su lugar, utilizamos información de ubicación anónima o aproximada (imprecisa) para proteger su privacidad." + } + }, "ja" : { "stringUnit" : { "state" : "translated", @@ -37918,6 +44119,12 @@ }, "To Radio (TX): %lld" : { "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "A radio (TX): %lld" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -37928,6 +44135,12 @@ }, "Topic: %@" : { "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Tema: %@" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -37962,6 +44175,12 @@ "value" : "Total" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Total" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -37990,6 +44209,12 @@ }, "Total PAX" : { "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "PAX TOTALES" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -38024,6 +44249,12 @@ }, "Trace Route" : { "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ruta de seguimiento" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -38052,6 +44283,12 @@ }, "Trace Route (in %@s)" : { "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ruta de seguimiento (en %@s)" + } + }, "ja" : { "stringUnit" : { "state" : "translated", @@ -38074,6 +44311,12 @@ }, "Trace Route Log" : { "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Registro de ruta de seguimiento" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -38114,6 +44357,12 @@ "value" : "Traceroute Ergebnis: %@" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Solicitud de ruta de seguimiento devuelta: %@" + } + }, "fr" : { "stringUnit" : { "state" : "translated", @@ -38172,6 +44421,12 @@ }, "Trace Route Sent" : { "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ruta de seguimiento enviada" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -38200,6 +44455,12 @@ }, "Trace route sent to %@" : { "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ruta de seguimiento enviada a %@" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -38228,6 +44489,12 @@ }, "Trace route to %@ was not sent." : { "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "La ruta de seguimiento a %@ no se envió." + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -38256,6 +44523,12 @@ }, "Trace Route was rate limited. You can send a trace route a maximum of once every thirty seconds." : { "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Trace Route tenía una tarifa limitada. Puede enviar una ruta de rastreo como máximo una vez cada treinta segundos." + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -38290,6 +44563,12 @@ "value" : "Standorte verfolgen und teilen" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Seguimiento y compartir ubicaciones" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -38300,6 +44579,12 @@ }, "Tracker" : { "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Rastreador" + } + }, "ja" : { "stringUnit" : { "state" : "translated", @@ -38334,6 +44619,12 @@ "value" : "Verkehr" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Tráfico" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -38362,6 +44653,12 @@ }, "Transmit data (txd) GPIO pin" : { "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Transmitir datos (txd) pin GPIO" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -38390,6 +44687,12 @@ }, "Transmit Enabled" : { "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Transmisión habilitada" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -38424,6 +44727,12 @@ }, "Treat double tap on supported accelerometers as a user button press." : { "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Considere el doble toque en los acelerómetros compatibles como si el usuario presionara un botón." + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -38458,6 +44767,12 @@ }, "TriggerType" : { "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Tipo de disparador" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -38486,6 +44801,12 @@ }, "Triple Click Ad Hoc Ping" : { "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ping ad hoc de triple clic" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -38520,6 +44841,12 @@ "value" : "Erneut versuchen" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Intentar otra vez" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -38554,6 +44881,12 @@ "value" : "Zwölf Stunden" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Doce horas" + } + }, "fr" : { "stringUnit" : { "state" : "translated", @@ -38618,6 +44951,12 @@ "value" : "Vierundzwanzig Stunden" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "veinticuatro horas" + } + }, "fr" : { "stringUnit" : { "state" : "translated", @@ -38682,6 +45021,12 @@ "value" : "Zwei Stunden" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "dos horas" + } + }, "fr" : { "stringUnit" : { "state" : "translated", @@ -38746,6 +45091,12 @@ "value" : "Zwei Minutes" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "dos minutos" + } + }, "fr" : { "stringUnit" : { "state" : "translated", @@ -38810,6 +45161,12 @@ "value" : "Zwei Sekunden" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "dos segundos" + } + }, "fr" : { "stringUnit" : { "state" : "translated", @@ -38868,6 +45225,12 @@ }, "UDP Broadcast" : { "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Transmisión UDP" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -38896,6 +45259,12 @@ }, "Ukraine 433MHz" : { "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ucrania 433MHz" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -38924,6 +45293,12 @@ }, "Ukraine 868MHz" : { "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ucrania 868MHz" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -38952,6 +45327,12 @@ }, "Un-Favorite" : { "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "No favorito" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -38980,6 +45361,12 @@ }, "Unhealthy" : { "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Malsano" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -39008,6 +45395,12 @@ }, "Unhealthy for Sensitive Groups" : { "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "No saludable para grupos sensibles" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -39036,6 +45429,12 @@ }, "United States" : { "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Estados Unidos" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -39064,6 +45463,12 @@ }, "Units displayed on the device screen" : { "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Unidades mostradas en la pantalla del dispositivo." + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -39092,6 +45497,12 @@ }, "unknown" : { "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "desconocido" + } + }, "it" : { "stringUnit" : { "state" : "needs_review", @@ -39120,6 +45531,12 @@ }, "Unknown" : { "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Desconocido" + } + }, "fr" : { "stringUnit" : { "state" : "translated", @@ -39184,6 +45601,12 @@ "value" : "Unbekanntes alter" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Edad desconocida" + } + }, "fr" : { "stringUnit" : { "state" : "translated", @@ -39248,6 +45671,12 @@ "value" : "Nicht benachrichtigbar" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "inmensable" + } + }, "ja" : { "stringUnit" : { "state" : "translated", @@ -39264,6 +45693,12 @@ }, "Unmonitored" : { "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "No monitoreado" + } + }, "ja" : { "stringUnit" : { "state" : "translated", @@ -39286,6 +45721,12 @@ "value" : "Unset" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Desarmado" + } + }, "fr" : { "stringUnit" : { "state" : "translated", @@ -39344,6 +45785,12 @@ }, "Unsupported" : { "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "No compatible" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -39378,6 +45825,12 @@ "value" : "Hoch" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Arriba" + } + }, "fr" : { "stringUnit" : { "state" : "translated", @@ -39436,6 +45889,12 @@ }, "Up Down 1" : { "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "arriba abajo 1" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -39468,8 +45927,24 @@ } } }, + "UPDATE IN" : { + "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "ACTUALIZAR EN" + } + } + } + }, "Update Interval" : { "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Intervalo de actualización" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -39504,6 +45979,12 @@ "value" : "Firmware aktualisieren" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Actualice su firmware" + } + }, "fr" : { "stringUnit" : { "state" : "translated", @@ -39562,6 +46043,12 @@ }, "Updated Node Stats Data." : { "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Datos de estadísticas de nodos actualizados." + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -39596,6 +46083,12 @@ "value" : "Aktualisiert: %@" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Actualizado: %@" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -39624,6 +46117,12 @@ }, "Uplink Enabled" : { "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Enlace ascendente habilitado" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -39664,6 +46163,12 @@ "value" : "Hochladen fehlgeschlagen" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Error de carga" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -39704,6 +46209,12 @@ "value" : "Lade GeoJSON-Dateien hoch, um eigene Karten-Overlays anzuzeigen. Die Dateien werden lokal gespeichert und dürfen bis zu 10 MB groß sein." } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Cargue archivos GeoJSON para mostrar superposiciones de mapas personalizados. Los archivos se almacenan localmente y pueden tener hasta 10 MB." + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -39739,6 +46250,12 @@ "Upload Map Data" : { "comment" : "Title for map data upload screen", "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Cargar datos del mapa" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -39780,6 +46297,12 @@ "value" : "Lade Kartendaten hoch, um Overlays zu aktivieren" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Cargar datos de mapas para habilitar superposiciones" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -39820,6 +46343,12 @@ "value" : "Kartendaten hochladen" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Cargar superposiciones de mapas" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -39836,6 +46365,12 @@ "value" : "Hochladen erfolgreich" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Subir con éxito" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -39876,6 +46411,12 @@ "value" : "Hochgeladene Kartendaten" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Superposiciones de mapas cargados" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -39886,6 +46427,12 @@ }, "Uptime" : { "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "tiempo de actividad" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -39932,6 +46479,12 @@ "value" : "Telemetriedaten erfassen" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Datos de uso y fallos" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -39942,6 +46495,12 @@ }, "Use a PWM output (like the RAK Buzzer) for tunes instead of an on/off output. This will ignore the output, output duration and active settings and use the device config buzzer GPIO option instead." : { "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Utilice una salida PWM (como el RAK Buzzer) para melodías en lugar de una salida de encendido/apagado. Esto ignorará la salida, la duración de la salida y la configuración activa y en su lugar utilizará la opción GPIO del zumbador de configuración del dispositivo." + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -39976,6 +46535,12 @@ }, "Use I2S As Buzzer" : { "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Utilice I2S como zumbador" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -40010,6 +46575,12 @@ "value" : "Standort verwenden" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Usar mi ubicación" + } + }, "ja" : { "stringUnit" : { "state" : "translated", @@ -40032,6 +46603,12 @@ "value" : "Voreinstellung verwenden" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Usar preajuste" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -40066,6 +46643,12 @@ }, "Use PWM Buzzer" : { "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Usar zumbador PWM" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -40106,6 +46689,12 @@ "value" : "Verwende das GPS deines Handys anstelle des GPS deines Funkgeräts." } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Utilice el GPS de su teléfono para enviar ubicaciones a su nodo en lugar de utilizar un GPS de hardware en su nodo." + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -40116,6 +46705,12 @@ }, "Used to create a shared key with a remote device." : { "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Se utiliza para crear una clave compartida con un dispositivo remoto." + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -40150,6 +46745,12 @@ "value" : "Wird verwendet, um nicht überwachte oder Infrastrukturknoten zu identifizieren, damit Nachrichten nicht an Knoten gesendet werden, die niemals antworten werden." } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Se utiliza para identificar nodos de infraestructura o no supervisados, de modo que la mensajería no esté disponible para nodos que nunca responderán." + } + }, "ja" : { "stringUnit" : { "state" : "translated", @@ -40172,6 +46773,12 @@ "value" : "Benutzer" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Usuario" + } + }, "fr" : { "stringUnit" : { "state" : "translated", @@ -40236,6 +46843,12 @@ "value" : "Benutzerkonfiguration" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Configuración de usuario" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -40276,6 +46889,12 @@ "value" : "Benutzerdaten" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Detalles del usuario" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -40310,6 +46929,12 @@ }, "User Id" : { "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Identificación de usuario" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -40354,6 +46979,12 @@ "value" : "Daten verfügbar" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Usuario subido" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -40394,6 +47025,12 @@ "value" : "Benutzername" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Nombre de usuario" + } + }, "fr" : { "stringUnit" : { "state" : "translated", @@ -40452,6 +47089,12 @@ }, "Uses pullup resistor" : { "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Utiliza resistencia pullup" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -40480,6 +47123,12 @@ }, "Utilizes the network connection on your phone to connect to MQTT." : { "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Utiliza la conexión de red de su teléfono para conectarse a MQTT." + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -40520,6 +47169,12 @@ "value" : "Fahrzeugsteuerkurs" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Rumbo del vehículo" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -40554,6 +47209,12 @@ "value" : "Fahrzeuggeschwindigkeit" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Velocidad del vehículo" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -40582,6 +47243,12 @@ }, "Verify who you are messaging with by comparing public keys in person or over the phone. The most recent public key for this node does not match the previously recorded key. You can delete the node and let it exchange keys again if the key change was due to a factory reset or other intentional action but this also may indicate a more serious security problem." : { "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Verifique con quién está enviando mensajes comparando claves públicas en persona o por teléfono. La clave pública más reciente para este nodo no coincide con la clave registrada anteriormente. Puede eliminar el nodo y dejar que intercambie claves nuevamente si el cambio de clave se debió a un restablecimiento de fábrica u otra acción intencional, pero esto también puede indicar un problema de seguridad más grave." + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -40598,6 +47265,12 @@ "value" : "Version %1$@ includes substantial network optimizations and extensive changes to devices and client apps. Only nodes version %2$@ and above are supported." } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "La versión %@ incluye optimizaciones sustanciales de la red y cambios extensos en los dispositivos y aplicaciones cliente. Solo se admiten nodos de versión %@ y superiores." + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -40638,6 +47311,12 @@ "value" : "Version: %1$@ (%2$@)" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Versión: %@ (%@)" + } + }, "ja" : { "stringUnit" : { "state" : "translated", @@ -40666,6 +47345,12 @@ "value" : "Version: %1$@ (%2$@) " } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Versión: %1$@ (%2$@)" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -40700,6 +47385,12 @@ }, "Very Unhealthy" : { "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "muy poco saludable" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -40734,6 +47425,12 @@ "value" : "Via Lora" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Vía Lora" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -40768,6 +47465,12 @@ "value" : "Via Mqtt" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Vía Mqtt" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -40808,6 +47511,12 @@ "value" : "Voltage" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Voltaje" + } + }, "fr" : { "stringUnit" : { "state" : "translated", @@ -40866,6 +47575,12 @@ }, "Volts %@" : { "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Voltios %@" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -40900,6 +47615,12 @@ "value" : "Warte..." } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Espera" + } + }, "fr" : { "stringUnit" : { "state" : "translated", @@ -40958,6 +47679,12 @@ }, "Waiting to be acknowledged. . ." : { "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Esperando ser reconocido. . ." + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -40986,6 +47713,12 @@ }, "Wake Screen on tap or motion" : { "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Activar pantalla con un toque o movimiento" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -41020,6 +47753,12 @@ "value" : "Gehen" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Caminando" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -41060,6 +47799,12 @@ "value" : "Welle" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ola" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -41094,6 +47839,12 @@ }, "Waypoint Failed to Send" : { "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "El punto de referencia no se pudo enviar" + } + }, "ja" : { "stringUnit" : { "state" : "translated", @@ -41116,6 +47867,12 @@ "value" : "Wegpunktoptionen" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Opciones de punto de referencia" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -41150,6 +47907,12 @@ "value" : "Wegpunkt von Knoten empfangen: %@" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Paquete de waypoint recibido del nodo: %@" + } + }, "fr" : { "stringUnit" : { "state" : "translated", @@ -41207,8 +47970,30 @@ } }, "Waypoints" : { - + "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Puntos de referencia" + } + } + } }, +<<<<<<< Updated upstream +======= + "We anonymously collect usage and crash data to improve the app. This helps us understand how the app is being used and where we can make improvements. The data we collect is non-personally identifiable and cannot be linked to you as an individual. You can opt out of this under app settings." : { + "comment" : "A description of how the app collects and uses user data. Includes a link to the app settings.", + "isCommentAutoGenerated" : true, + "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Recopilamos de forma anónima datos de uso y fallos para mejorar la aplicación. Esto nos ayuda a comprender cómo se utiliza la aplicación y dónde podemos realizar mejoras. Los datos que recopilamos no son identificables personalmente y no pueden vincularse a usted como individuo. Puede optar por no participar en la configuración de la aplicación." + } + } + } + }, +>>>>>>> Stashed changes "Weather Conditions" : { "localizations" : { "de" : { @@ -41217,6 +48002,12 @@ "value" : "Wetterverhältnisse" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Condiciones climáticas" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -41251,6 +48042,12 @@ }, "Web Flasher" : { "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Intermitente web" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -41279,6 +48076,12 @@ }, "Website" : { "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Sitio web" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -41313,6 +48116,12 @@ }, "Weight" : { "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Peso" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -41347,6 +48156,12 @@ "value" : "Willkommen bei" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Bienvenido a" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -41363,6 +48178,12 @@ "value" : "Was bedeutet das Schloß?" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "¿Qué significa la cerradura?" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -41403,6 +48224,12 @@ "value" : "Was ist Meshtastic?" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "¿Qué es Meshtastic?" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -41437,6 +48264,12 @@ }, "What licensed operator mode does:\n* Sets the node name to your call sign \n* Broadcasts node info every 10 minutes \n* Overrides frequency, dutycycle and tx power \n* Disables encryption" : { "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Qué hace el modo de operador con licencia:\n* Establece el nombre del nodo según su indicativo de llamada \n* Transmite información del nodo cada 10 minutos \n* Anula la frecuencia, el ciclo de trabajo y la potencia de transmisión. \n* Desactiva el cifrado" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -41471,6 +48304,12 @@ }, "When enabled the PAX Counter module counts the number of people passing by using WiFi and Bluetooth. Both WiFI and Bluetooth must be disabled for PAX counter to work." : { "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Cuando está habilitado, el módulo Contador de PAX cuenta el número de personas que pasan mediante WiFi y Bluetooth. Tanto WiFI como Bluetooth deben estar desactivados para que funcione el contador de PAX." + } + }, "he" : { "stringUnit" : { "state" : "translated", @@ -41517,6 +48356,12 @@ }, "When using in GPIO mode, keep the output on for this long. " : { "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Cuando lo use en modo GPIO, mantenga la salida encendida durante este tiempo." + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -41551,6 +48396,12 @@ }, "Whether or not use INPUT_PULLUP mode for GPIO pin. Only applicable if the board uses pull-up resistors on the pin" : { "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Si se utiliza o no el modo INPUT_PULLUP para el pin GPIO. Solo aplicable si la placa usa resistencias pull-up en el pin" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -41579,6 +48430,12 @@ }, "WiFi" : { "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Wi-Fi" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -41619,6 +48476,12 @@ "value" : "WiFi Optionen" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Opciones WiFi" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -41647,6 +48510,12 @@ }, "Will sleep everything as much as possible, for the tracker and sensor role this will also include the lora radio. Don't use this setting if you want to use your device with the phone apps or are using a device without a user button." : { "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Dormirá todo lo más posible, para la función de rastreador y sensor esto también incluirá la radio lora. No use esta configuración si desea usar su dispositivo con las aplicaciones del teléfono o si está usando un dispositivo sin un botón de usuario." + } + }, "he" : { "stringUnit" : { "state" : "translated", @@ -41699,6 +48568,12 @@ }, "Wind" : { "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Viento" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -41733,6 +48608,12 @@ "value" : "Windrichtung" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Dirección del viento" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -41767,6 +48648,12 @@ "value" : "Windgeschwindigkeit" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Velocidad del viento" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -41801,6 +48688,12 @@ "value" : "Innerhalb %@" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Dentro %@" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -41835,6 +48728,12 @@ }, "x" : { "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "incógnita" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -41869,6 +48768,12 @@ "value" : "X: %1$@, Y: %2$d" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "X: %@, Y: %d" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -41910,6 +48815,12 @@ "value" : "X: %1$@, Y: %2$f" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "X: %@, Y: %f" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -41951,6 +48862,12 @@ "value" : "X: %1$@, Y: %2$lld" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "X: %@, Y: %lld" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -41986,6 +48903,12 @@ }, "y" : { "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "y" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -42030,6 +48953,12 @@ "value" : "Gestern" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ayer" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -42058,6 +48987,12 @@ }, "You can also update your Meshtastic device over bluetooth using the Nordic DFU app." : { "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "También puede actualizar su dispositivo Meshtastic a través de bluetooth utilizando la aplicación Nordic DFU." + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -42098,6 +49033,12 @@ "value" : "Du kannst Kanalnachrichten (Gruppenchats) und Direktnachrichten senden und empfangen. Bei jeder Nachricht kannst du lange drücken, um verfügbare Aktionen wie Kopieren, Antworten, Tapback und Löschen sowie Zustelldetails anzuzeigen." } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Puedes enviar y recibir canales (chats grupales) y mensajes directos. Desde cualquier mensaje, puede mantener presionado para ver las acciones disponibles como copiar, responder, retroceder y eliminar, así como los detalles de entrega." + } + }, "fr" : { "stringUnit" : { "state" : "translated", @@ -42156,6 +49097,12 @@ }, "Your current location will be set as the fixed position and broadcast over the mesh on the position interval." : { "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Su ubicación actual se establecerá como posición fija y se transmitirá sobre la malla en el intervalo de posición." + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -42196,6 +49143,12 @@ "value" : "Deine Firmware ist aktuell" } }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Su firmware está actualizado" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -42230,6 +49183,12 @@ }, "Your MQTT Server must support TLS." : { "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Su servidor MQTT debe admitir TLS." + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -42258,6 +49217,12 @@ }, "Your node will periodically send an unencrypted map report packet to the configured MQTT server, this includes id, short and long name, approximate location, hardware model, role, firmware version, LoRa region, modem preset and primary channel name." : { "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Su nodo enviará periódicamente un paquete de informe de mapa sin cifrar al servidor MQTT configurado, esto incluye identificación, nombre corto y largo, ubicación aproximada, modelo de hardware, función, versión de firmware, región LoRa, configuración predeterminada del módem y nombre del canal principal." + } + }, "ja" : { "stringUnit" : { "state" : "translated", @@ -42280,6 +49245,12 @@ }, "Your node’s operating frequency is calculated based on the region, modem preset, and this field. When 0, the slot is automatically calculated based on the primary channel name." : { "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "La frecuencia operativa de su nodo se calcula en función de la región, la configuración predeterminada del módem y este campo. Cuando es 0, la ranura se calcula automáticamente en función del nombre del canal principal." + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -42308,6 +49279,12 @@ }, "Your position has been sent with a request for a response with their position. You will receive a notification when a position is returned." : { "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Su posición ha sido enviada con una solicitud de respuesta con su posición. Recibirá una notificación cuando se devuelva una posición." + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -42341,10 +49318,23 @@ } }, "Your public key is generated from your private key and sent to other nodes on the mesh so they can compute a shared secret key with you." : { - + "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Su clave pública se genera a partir de su clave privada y se envía a otros nodos de la malla para que puedan calcular una clave secreta compartida con usted." + } + } + } }, "Your region has a %lld%% duty cycle. MQTT is not advised when you are duty cycle restricted, the extra traffic will quickly overwhelm your LoRa mesh." : { "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Su región tiene un ciclo de trabajo %lld%%. No se recomienda MQTT cuando tiene un ciclo de trabajo restringido, el tráfico adicional abrumará rápidamente su malla LoRa." + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -42379,6 +49369,12 @@ }, "Your region has a %lld%% hourly duty cycle, your radio will stop sending packets when it reaches the hourly limit." : { "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Su región tiene un ciclo de trabajo por hora %lld%%, su radio dejará de enviar paquetes cuando alcance el límite por hora." + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -42407,6 +49403,12 @@ }, "Your route file must have both Latitude and Longitude columns and headers." : { "localizations" : { + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Su archivo de ruta debe tener columnas y encabezados de Latitud y Longitud." + } + }, "it" : { "stringUnit" : { "state" : "translated", From 575cb887b0d057cdf138d1df43ba1a016a08ea38 Mon Sep 17 00:00:00 2001 From: Mathew Kamkar <578302+matkam@users.noreply.github.com> Date: Sun, 4 Jan 2026 20:26:51 -0800 Subject: [PATCH 57/68] tapback with any emoji (#1538) --- Localizable.xcstrings | 3 + Meshtastic.xcodeproj/project.pbxproj | 4 + Meshtastic/Helpers/EmojiOnlyTextField.swift | 46 ++++++++ .../Messages/MessageContextMenuItems.swift | 27 +---- Meshtastic/Views/Messages/MessageText.swift | 36 +++++- .../Views/Messages/TapbackInputView.swift | 108 ++++++++++++++++++ 6 files changed, 198 insertions(+), 26 deletions(-) create mode 100644 Meshtastic/Views/Messages/TapbackInputView.swift diff --git a/Localizable.xcstrings b/Localizable.xcstrings index 78c9454b..877f87f1 100644 --- a/Localizable.xcstrings +++ b/Localizable.xcstrings @@ -36916,6 +36916,9 @@ } } } + }, + "Select an emoji" : { + }, "Select Channel" : { "localizations" : { diff --git a/Meshtastic.xcodeproj/project.pbxproj b/Meshtastic.xcodeproj/project.pbxproj index cd52c496..a504bb13 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 */; }; @@ -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 = ""; }; @@ -1250,6 +1252,7 @@ D93068D62B8146690066FBC8 /* MessageText.swift */, D93068D22B8129510066FBC8 /* MessageContextMenuItems.swift */, D93068D82B81509C0066FBC8 /* TapbackResponses.swift */, + D93068D92B81509D0066FBC8 /* TapbackInputView.swift */, ); path = Messages; sourceTree = ""; @@ -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 */, diff --git a/Meshtastic/Helpers/EmojiOnlyTextField.swift b/Meshtastic/Helpers/EmojiOnlyTextField.swift index 0982ab33..4928da73 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 emoji, false otherwise + 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/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..98734b24 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 + ) + Task { @MainActor in + 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..4b961295 --- /dev/null +++ b/Meshtastic/Views/Messages/TapbackInputView.swift @@ -0,0 +1,108 @@ +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) { + 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 + } + }, + 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) + .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.. Date: Sun, 4 Jan 2026 20:28:40 -0800 Subject: [PATCH 58/68] Call clearStaleNodes at start of sendWantConfig (#1535) --- Localizable.xcstrings | 4 ++++ Meshtastic/Accessory/Accessory Manager/AccessoryManager.swift | 2 ++ Meshtastic/Extensions/UserDefaults.swift | 4 ++++ Meshtastic/Views/Settings/AppSettings.swift | 2 +- 4 files changed, 11 insertions(+), 1 deletion(-) diff --git a/Localizable.xcstrings b/Localizable.xcstrings index 877f87f1..66249be2 100644 --- a/Localizable.xcstrings +++ b/Localizable.xcstrings @@ -16733,6 +16733,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" : { @@ -16759,6 +16760,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" : { diff --git a/Meshtastic/Accessory/Accessory Manager/AccessoryManager.swift b/Meshtastic/Accessory/Accessory Manager/AccessoryManager.swift index 07513866..fc352614 100644 --- a/Meshtastic/Accessory/Accessory Manager/AccessoryManager.swift +++ b/Meshtastic/Accessory/Accessory Manager/AccessoryManager.swift @@ -196,6 +196,8 @@ class AccessoryManager: ObservableObject, MqttClientProxyManagerDelegate { Logger.transport.error("Unable to send wantConfig (config): No device connected") return } + + _ = clearStaleNodes(nodeExpireDays: Int(UserDefaults.purgeStaleNodeDays), context: self.context) try await withTaskCancellationHandler { var toRadio: ToRadio = ToRadio() 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/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) } From 5c22b8b6e0176f4927bfc79234dabe109b215edf Mon Sep 17 00:00:00 2001 From: Benjamin Faershtein <119711889+RCGV1@users.noreply.github.com> Date: Sun, 4 Jan 2026 20:38:17 -0800 Subject: [PATCH 59/68] NFC Tag contact (#1537) --- Localizable.xcstrings | 17 ++- Meshtastic/Info.plist | 2 + Meshtastic/Meshtastic.entitlements | 4 + Meshtastic/Router/NavigationState.swift | 1 + Meshtastic/Views/Settings/Settings.swift | 9 ++ Meshtastic/Views/Settings/Tools.swift | 164 +++++++++++++++++++++++ 6 files changed, 196 insertions(+), 1 deletion(-) create mode 100644 Meshtastic/Views/Settings/Tools.swift diff --git a/Localizable.xcstrings b/Localizable.xcstrings index 66249be2..ca97d123 100644 --- a/Localizable.xcstrings +++ b/Localizable.xcstrings @@ -10693,6 +10693,10 @@ } } }, + "Create Node Contact NFC Tag" : { + "comment" : "A section header that instructs the user to create a contact NFC tag.", + "isCommentAutoGenerated" : true + }, "Create Waypoint" : { "localizations" : { "de" : { @@ -27747,6 +27751,10 @@ } } }, + "Node Name: %@" : { + "comment" : "A text label displaying the name of the connected node.", + "isCommentAutoGenerated" : true + }, "Node Number" : { "localizations" : { "de" : { @@ -44139,6 +44147,9 @@ } } } + }, + "Tools" : { + }, "Topic: %@" : { "localizations" : { @@ -48733,6 +48744,10 @@ } } }, + "Write Contact to NFC Tag" : { + "comment" : "A button that writes a contact to an NFC tag.", + "isCommentAutoGenerated" : true + }, "x" : { "localizations" : { "es" : { @@ -49444,4 +49459,4 @@ } }, "version" : "1.1" -} +} \ No newline at end of file diff --git a/Meshtastic/Info.plist b/Meshtastic/Info.plist index 863fb0e9..c2cbcdd8 100644 --- a/Meshtastic/Info.plist +++ b/Meshtastic/Info.plist @@ -97,6 +97,8 @@ LSSupportsOpeningDocumentsInPlace + NFCReaderUsageDescription + We use NFC tags to share node contacts NSBluetoothAlwaysUsageDescription We use bluetooth to connect to nearby Meshtastic Devices NSBluetoothPeripheralUsageDescription diff --git a/Meshtastic/Meshtastic.entitlements b/Meshtastic/Meshtastic.entitlements index 4dbdb836..a35e74ee 100644 --- a/Meshtastic/Meshtastic.entitlements +++ b/Meshtastic/Meshtastic.entitlements @@ -9,6 +9,10 @@ com.apple.developer.carplay-communication + com.apple.developer.nfc.readersession.formats + + TAG + com.apple.developer.usernotifications.critical-alerts com.apple.developer.weatherkit diff --git a/Meshtastic/Router/NavigationState.swift b/Meshtastic/Router/NavigationState.swift index 48a97b93..8c2ff6b3 100644 --- a/Meshtastic/Router/NavigationState.swift +++ b/Meshtastic/Router/NavigationState.swift @@ -52,6 +52,7 @@ enum SettingsNavigationState: String { case debugLogs case appFiles case firmwareUpdates + case tools } struct NavigationState: Hashable { diff --git a/Meshtastic/Views/Settings/Settings.swift b/Meshtastic/Views/Settings/Settings.swift index d3d15a66..25b0e75a 100644 --- a/Meshtastic/Views/Settings/Settings.swift +++ b/Meshtastic/Views/Settings/Settings.swift @@ -355,6 +355,13 @@ struct Settings: View { Image(systemName: "gearshape") } } + NavigationLink(value: SettingsNavigationState.tools) { + Label { + Text("Tools") + } icon: { + Image(systemName: "hammer") + } + } NavigationLink(value: SettingsNavigationState.routes) { Label { Text("Routes") @@ -521,6 +528,8 @@ struct Settings: View { AppData() case .firmwareUpdates: Firmware(node: node) + case .tools: + Tools() } } .onChange(of: UserDefaults.preferredPeripheralNum ) { _, newConnectedNode in diff --git a/Meshtastic/Views/Settings/Tools.swift b/Meshtastic/Views/Settings/Tools.swift new file mode 100644 index 00000000..75e439de --- /dev/null +++ b/Meshtastic/Views/Settings/Tools.swift @@ -0,0 +1,164 @@ +// +// Tools.swift +// Meshtastic +// +// Created by Benjamin Faershtein on 12/31/25. +// + +import SwiftUI +import CoreNFC +import MeshtasticProtobufs +import OSLog + +struct Tools: View { + @EnvironmentObject var accessoryManager: AccessoryManager + @Environment(\.managedObjectContext) var context + + @StateObject private var nfcReader = NFCReader() + + var connectedNode: NodeInfoEntity? { + if let num = accessoryManager.activeDeviceNum { + return getNodeInfo(id: num, context: context) + } + return nil + } + + var qrString: String { + var contact = SharedContact() + contact.nodeNum = UInt32(connectedNode?.num ?? 0) + contact.user = connectedNode?.toProto().user ?? User() + contact.manuallyVerified = true + + do { + let contactString = try contact.serializedData().base64EncodedString() + return "https://meshtastic.org/v/#" + contactString.base64ToBase64url() + } catch { + Logger.services.error("Error serializing contact: \(error)") + return "" + } + } + + var body: some View { + VStack{ + List { + Section(header: Text("Create Node Contact NFC Tag")) { + if let node = connectedNode { + Text("Node Name: \(node.user?.longName ?? "Unknown")") + + Button { + nfcReader.scan(theActualData: qrString) + } label: { + Label("Write Contact to NFC Tag", systemImage: "tag") + } + .disabled(qrString.isEmpty) + } + } + } + } + .navigationTitle("Tools") + .navigationBarTitleDisplayMode(.inline) + } +} + +#Preview { + Tools() +} + +final class NFCReader: NSObject, ObservableObject, NFCNDEFReaderSessionDelegate { + + private let logger = Logger(subsystem: "org.meshtastic.app", category: "NFC") + private var payloadString = "" + private var session: NFCNDEFReaderSession? + + func scan(theActualData: String) { + payloadString = theActualData + + session = NFCNDEFReaderSession( + delegate: self, + queue: nil, + invalidateAfterFirstRead: false + ) + + session?.alertMessage = "Hold your iPhone near the NFC tag." + session?.begin() + } + + func readerSessionDidBecomeActive(_ session: NFCNDEFReaderSession) { + logger.debug("NFC session became active") + } + + func readerSession(_ session: NFCNDEFReaderSession, + didInvalidateWithError error: Error) { + logger.error("NFC session invalidated: \(error.localizedDescription)") + } + + func readerSession(_ session: NFCNDEFReaderSession, + didDetectNDEFs messages: [NFCNDEFMessage]) { + } + + func readerSession(_ session: NFCNDEFReaderSession, + didDetect tags: [NFCNDEFTag]) { + + guard tags.count == 1, let tag = tags.first else { + session.alertMessage = "More than one tag detected. Please present only one." + DispatchQueue.global().asyncAfter(deadline: .now() + .milliseconds(500)) { + session.restartPolling() + } + return + } + + session.connect(to: tag) { error in + if let error { + self.logger.error("Failed to connect to tag: \(error.localizedDescription)") + session.alertMessage = "Failed to connect to tag." + session.invalidate() + return + } + + tag.queryNDEFStatus { status, _, error in + if let error { + self.logger.error("Failed to query NDEF status: \(error.localizedDescription)") + session.alertMessage = "Failed to read tag." + session.invalidate() + return + } + + switch status { + case .notSupported: + self.logger.error("Tag does not support NDEF") + session.alertMessage = "Tag does not support NDEF." + session.invalidate() + + case .readOnly: + self.logger.error("Tag is read-only") + session.alertMessage = "Tag is read-only." + session.invalidate() + + case .readWrite: + guard let payload = + NFCNDEFPayload.wellKnownTypeURIPayload( + string: self.payloadString + ) else { + self.logger.error("Invalid NDEF payload") + session.alertMessage = "Invalid payload." + session.invalidate() + return + } + + let message = NFCNDEFMessage(records: [payload]) + + tag.writeNDEF(message) { error in + if let error { + self.logger.error("Failed to write NDEF: \(error.localizedDescription)") + session.alertMessage = "Failed to write tag." + } else { + self.logger.info("Successfully wrote NFC tag") + session.alertMessage = "NFC tag written successfully." + } + session.invalidate() + } + } + } + } + } +} From 487f24b99a4f3d0b4491ee7a2c86dcffb7f62c7f Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Sun, 4 Jan 2026 20:38:39 -0800 Subject: [PATCH 60/68] 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> --- .../Accessory Manager/AccessoryManager+Discovery.swift | 8 ++++++-- .../Accessory/Accessory Manager/AccessoryManager.swift | 1 + Meshtastic/MeshtasticApp.swift | 1 + 3 files changed, 8 insertions(+), 2 deletions(-) diff --git a/Meshtastic/Accessory/Accessory Manager/AccessoryManager+Discovery.swift b/Meshtastic/Accessory/Accessory Manager/AccessoryManager+Discovery.swift index 831ffe30..266b6945 100644 --- a/Meshtastic/Accessory/Accessory Manager/AccessoryManager+Discovery.swift +++ b/Meshtastic/Accessory/Accessory Manager/AccessoryManager+Discovery.swift @@ -52,8 +52,12 @@ extension AccessoryManager { existing.rssi = newDevice.rssi self.devices[index] = existing } else { - // This is a new device, add it to our list - self.devices.append(newDevice) + // This is a new device, add it to our list if we are in the foreground + if !(self.isInBackground) { + self.devices.append(newDevice) + } else { + Logger.transport.debug("🔎 [Discovery] Found a new device but not in the foreground, not adding to our list: peripheral \(newDevice.name)") + } } if self.shouldAutomaticallyConnectToPreferredPeripheral, diff --git a/Meshtastic/Accessory/Accessory Manager/AccessoryManager.swift b/Meshtastic/Accessory/Accessory Manager/AccessoryManager.swift index fc352614..91f13287 100644 --- a/Meshtastic/Accessory/Accessory Manager/AccessoryManager.swift +++ b/Meshtastic/Accessory/Accessory Manager/AccessoryManager.swift @@ -135,6 +135,7 @@ class AccessoryManager: ObservableObject, MqttClientProxyManagerDelegate { @Published var lastConnectionError: Error? @Published var isConnected: Bool = false @Published var isConnecting: Bool = false + @Published var isInBackground: Bool = false var activeConnection: (device: Device, connection: any Connection)? diff --git a/Meshtastic/MeshtasticApp.swift b/Meshtastic/MeshtasticApp.swift index d60ed940..5c42dd22 100644 --- a/Meshtastic/MeshtasticApp.swift +++ b/Meshtastic/MeshtasticApp.swift @@ -193,6 +193,7 @@ struct MeshtasticAppleApp: App { } } .onChange(of: scenePhase) { (_, newScenePhase) in + accessoryManager.isInBackground = (newScenePhase == .background) switch newScenePhase { case .background: Logger.services.info("🎬 [App] Scene is in the background") From 2caba22a352ab178d1b1fdffd7c1e26b9a20a78b Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Sun, 4 Jan 2026 20:52:40 -0800 Subject: [PATCH 61/68] Revert "Full translation into Spanish (#1529)" (#1543) This reverts commit f25fdfb89fba70d22cf1d281c62f956f94d6343c. --- Localizable.xcstrings | 7024 +---------------------------------------- 1 file changed, 11 insertions(+), 7013 deletions(-) diff --git a/Localizable.xcstrings b/Localizable.xcstrings index ca97d123..b47fe554 100644 --- a/Localizable.xcstrings +++ b/Localizable.xcstrings @@ -6,12 +6,6 @@ }, "\t%@" : { "localizations" : { - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "\t%@" - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -47,12 +41,6 @@ }, " %@" : { "localizations" : { - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : " %@" - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -146,12 +134,6 @@ }, ": %d" : { "localizations" : { - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : ": %d" - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -187,12 +169,6 @@ }, "(Re)define PIN_GPS_EN for your board." : { "localizations" : { - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "(Re)defina PIN_GPS_EN para su placa." - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -227,12 +203,6 @@ }, "%@" : { "localizations" : { - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "%@" - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -356,12 +326,6 @@ "value" : "%1$@ - %2$@ Towards %3$@ Back" } }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "%@ - %@ Hacia %@ Atrás" - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -402,12 +366,6 @@ "value" : "%@ - Keine Antwort" } }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "%@ - Sin respuesta" - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -448,12 +406,6 @@ "value" : "%@ - Nicht gesendet" } }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "%@ - No enviado" - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -576,12 +528,6 @@ "value" : "%1$@ %2$lld" } }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "%@ %lld" - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -623,12 +569,6 @@ "value" : "%@ entfernt" } }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "%@ lejos" - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -669,12 +609,6 @@ "value" : "%@ kann bis zu %@ Byte lang sein." } }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "%@ puede tener hasta %@ bytes de longitud." - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -709,12 +643,6 @@ }, "%@ Channels?" : { "localizations" : { - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "%@ ¿Canales?" - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -749,12 +677,6 @@ }, "%@ config data was requested via PKC admin but no response has been returned from the remote node." : { "localizations" : { - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Se solicitaron %@ datos de configuración a través del administrador de PKC pero no se obtuvo respuesta del nodo remoto." - } - }, "sr" : { "stringUnit" : { "state" : "translated", @@ -765,12 +687,6 @@ }, "%@ dB" : { "localizations" : { - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "%@dB" - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -852,12 +768,6 @@ "value" : "%1$@: %2$lld / %3$lld" } }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "%@: %lld / %lld" - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -893,12 +803,6 @@ }, "%@%%" : { "localizations" : { - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "%@%%" - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -933,12 +837,6 @@ }, "%@°F" : { "localizations" : { - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "%@°F" - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -973,12 +871,6 @@ }, "%@mA" : { "localizations" : { - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "%@mA" - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -1007,12 +899,6 @@ }, "%@V" : { "localizations" : { - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "%@V" - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -1041,12 +927,6 @@ }, "%d" : { "localizations" : { - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "%d" - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -1099,12 +979,6 @@ } } }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "%d saltos" - } - }, "it" : { "variations" : { "plural" : { @@ -1187,12 +1061,6 @@ }, "%d%%" : { "localizations" : { - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "%d%%" - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -1227,12 +1095,6 @@ }, "%f%%" : { "localizations" : { - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "%f%%" - } - }, "sr" : { "stringUnit" : { "state" : "translated", @@ -1243,12 +1105,6 @@ }, "%lf" : { "localizations" : { - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "%lf" - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -1283,12 +1139,6 @@ }, "%lld" : { "localizations" : { - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "%lld" - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -1329,12 +1179,6 @@ "value" : "%1$lld %2$@" } }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "%lld %@" - } - }, "sr" : { "stringUnit" : { "state" : "translated", @@ -1381,12 +1225,6 @@ } } }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "%lld características" - } - }, "sr" : { "variations" : { "plural" : { @@ -1421,12 +1259,6 @@ "value" : "%lld oder weniger Hops entfernt" } }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "%lld o menos salta de distancia" - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -1455,12 +1287,6 @@ }, "%lld Readings Total" : { "localizations" : { - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "%lld lecturas totales" - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -1489,12 +1315,6 @@ }, "%lld Total Detection Events" : { "localizations" : { - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "%lld total de eventos de detección" - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -1523,12 +1343,6 @@ }, "%lld%%" : { "localizations" : { - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "%lld%%" - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -1569,12 +1383,6 @@ "value" : "%llddb Übertragungsleistung" } }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "%llddb Potencia de transmisión" - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -1615,12 +1423,6 @@ "value" : "%llddBm Übertragungsleistung" } }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "%llddBm Potencia de transmisión" - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -1652,12 +1454,6 @@ }, "< 1%" : { "localizations" : { - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "< 1%" - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -1690,32 +1486,12 @@ } } }, -<<<<<<< Updated upstream "⚠️ The configured value: (%@) is not one of the optimized options." : { "comment" : "A warning label below the picker, indicating that the selected update interval is not one of the optimized options.", "isCommentAutoGenerated" : true -======= - "⚠️ The configured value: (%@ seconds) is not one of the optimized options." : { - "comment" : "A text warning that the configured update interval is not one of the optimized options.", - "isCommentAutoGenerated" : true, - "localizations" : { - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "⚠️ El valor configurado: (%@ segundos) no es una de las opciones optimizadas." - } - } - } ->>>>>>> Stashed changes }, "🦕 End of life Version 🦖 ☄️" : { "localizations" : { - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "🦕 Versión de fin de vida 🦖 ☄️" - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -1744,12 +1520,6 @@ }, "0" : { "localizations" : { - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "0" - } - }, "ja" : { "stringUnit" : { "state" : "translated", @@ -1761,12 +1531,6 @@ }, "1" : { "localizations" : { - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "1" - } - }, "ja" : { "stringUnit" : { "state" : "translated", @@ -1778,12 +1542,6 @@ }, "1 byte" : { "localizations" : { - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "1 byte" - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -1818,12 +1576,6 @@ }, "1 hop away" : { "localizations" : { - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "1 salto de distancia" - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -1858,12 +1610,6 @@ }, "2.4 Ghz" : { "localizations" : { - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "2,4 GHz" - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -1892,12 +1638,6 @@ }, "7" : { "localizations" : { - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "7" - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -1932,12 +1672,6 @@ }, "12 Hour Clock" : { "localizations" : { - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Reloj de 12 horas" - } - }, "ja" : { "stringUnit" : { "state" : "translated", @@ -1954,12 +1688,6 @@ }, "25" : { "localizations" : { - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "25" - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -1994,12 +1722,6 @@ }, "50" : { "localizations" : { - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "50" - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -2034,12 +1756,6 @@ }, "75" : { "localizations" : { - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "75" - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -2074,12 +1790,6 @@ }, "100" : { "localizations" : { - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "100" - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -2114,12 +1824,6 @@ }, "128 bit" : { "localizations" : { - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "128 bits" - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -2154,12 +1858,6 @@ }, "180" : { "localizations" : { - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "180" - } - }, "ja" : { "stringUnit" : { "state" : "translated", @@ -2171,12 +1869,6 @@ }, "256 bit" : { "localizations" : { - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "256 bits" - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -2211,12 +1903,6 @@ }, "A channel index of 0 indicates the primary channel where broadcast packets are sent from. Location data is broadcast from the first channel where it is enabled with firmware 2.7 forward." : { "localizations" : { - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Un índice de canal de 0 indica el canal principal desde donde se envían los paquetes de transmisión. Los datos de ubicación se transmiten desde el primer canal donde está habilitado con el firmware 2.7 en adelante." - } - }, "sr" : { "stringUnit" : { "state" : "translated", @@ -2227,12 +1913,6 @@ }, "A green lock means the channel is securely encrypted with either a 128 or 256 bit AES key." : { "localizations" : { - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Un candado verde significa que el canal está cifrado de forma segura con una clave AES de 128 o 256 bits." - } - }, "ja" : { "stringUnit" : { "state" : "translated", @@ -2255,12 +1935,6 @@ "value" : "In a Meshtastic LoRa Mesh there are up to 8 channels. The first one is the Primary channel where most activity happens and is required. If you don't share your primary channel your first shared channel becomes the primary channel on the other network. It talks on its primary and your secondary channel. A channel with the name 'admin' controls nodes remotely. Other channels are for private groups, each with its own key." } }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Un código QR Meshtastic contiene la configuración de LoRa y los valores de canal necesarios para que las radios se comuniquen. Puede compartir una configuración de canal completa usando la opción Reemplazar canales; si elige Agregar canales, sus canales compartidos se agregarán a los canales de la radio receptora." - } - }, "fr" : { "stringUnit" : { "state" : "translated", @@ -2319,12 +1993,6 @@ }, "A red open lock means the channel is not securely encrypted and is used for precise location data, it uses either no key at all or a 1 byte known key." : { "localizations" : { - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Un candado rojo abierto significa que el canal no está cifrado de forma segura y se utiliza para datos de ubicación precisos; no utiliza ninguna clave o utiliza una clave conocida de 1 byte." - } - }, "sr" : { "stringUnit" : { "state" : "translated", @@ -2335,12 +2003,6 @@ }, "A red open lock with a warning means the channel is not securely encrypted and is used for precise location data which is being uplinked to the internet via MQTT, it uses either no key at all or a 1 byte known key." : { "localizations" : { - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Un candado rojo abierto con una advertencia significa que el canal no está cifrado de forma segura y se utiliza para datos de ubicación precisos que se conectan a Internet a través de MQTT; no utiliza ninguna clave o utiliza una clave conocida de 1 byte." - } - }, "sr" : { "stringUnit" : { "state" : "translated", @@ -2351,12 +2013,6 @@ }, "A Trace Route was sent, no response has been received." : { "localizations" : { - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Se envió una ruta de rastreo y no se recibió respuesta." - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -2386,12 +2042,6 @@ "A yellow open lock lock means the channel is not securely encrypted but it not used for precise location data, it uses either no key at all or a 1 byte known key." : { "extractionState" : "stale", "localizations" : { - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Un candado abierto amarillo significa que el canal no está cifrado de forma segura pero no se utiliza para datos de ubicación precisos; no utiliza ninguna clave o utiliza una clave conocida de 1 byte." - } - }, "sr" : { "stringUnit" : { "state" : "translated", @@ -2402,15 +2052,7 @@ }, "A yellow open lock means the channel is not securely encrypted but it is not used for precise location data, it uses either no key at all or a 1 byte known key." : { "comment" : "A description of a yellow open lock in the Channels Help view.", - "isCommentAutoGenerated" : true, - "localizations" : { - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Un candado abierto amarillo significa que el canal no está cifrado de forma segura, pero no se utiliza para datos de ubicación precisos; no utiliza ninguna clave o utiliza una clave conocida de 1 byte." - } - } - } + "isCommentAutoGenerated" : true }, "About" : { "localizations" : { @@ -2420,12 +2062,6 @@ "value" : "Über Meshtastic" } }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Acerca de" - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -2460,12 +2096,6 @@ "value" : "Über Meshtastic" } }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Acerca de Meshtastic" - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -2506,12 +2136,6 @@ "value" : "Genauigkeit %@" } }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Exactitud %@" - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -2540,12 +2164,6 @@ }, "Ack SNR: %@ dB" : { "localizations" : { - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Confirmación SNR: %@ dB" - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -2574,12 +2192,6 @@ }, "Ack Time: %@" : { "localizations" : { - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Tiempo de confirmación: %@" - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -2614,12 +2226,6 @@ "value" : "Bestätigt" } }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Admitido" - } - }, "fr" : { "stringUnit" : { "state" : "translated", @@ -2678,12 +2284,6 @@ }, "Acknowledged by another node" : { "localizations" : { - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Reconocido por otro nodo" - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -2718,12 +2318,6 @@ "value" : "Aktionen" } }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Comportamiento" - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -2758,12 +2352,6 @@ "value" : "Aktiv" } }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Activo" - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -2798,12 +2386,6 @@ "value" : "Aktivität" } }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Actividad" - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -2844,12 +2426,6 @@ "value" : "ADC Override" } }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Anulación del ADC" - } - }, "he" : { "stringUnit" : { "state" : "translated", @@ -2896,12 +2472,6 @@ }, "Add Channel" : { "localizations" : { - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Agregar canal" - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -2930,12 +2500,6 @@ }, "Add Channels" : { "localizations" : { - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Agregar canales" - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -2964,12 +2528,6 @@ }, "Add Contact" : { "localizations" : { - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Agregar contacto" - } - }, "ja" : { "stringUnit" : { "state" : "translated", @@ -2992,12 +2550,6 @@ }, "Add Meshtastic Node %@ as a contact" : { "localizations" : { - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Agregar Meshtastic Node %@ como contacto" - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -3032,12 +2584,6 @@ "value" : "Zu Favoriten hinzufügen" } }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Añadir a favoritos" - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -3066,12 +2612,6 @@ }, "Additional help" : { "localizations" : { - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Ayuda adicional" - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -3106,12 +2646,6 @@ }, "Address" : { "localizations" : { - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Dirección" - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -3146,12 +2680,6 @@ }, "Admin Keys" : { "localizations" : { - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Claves de administrador" - } - }, "ja" : { "stringUnit" : { "state" : "translated", @@ -3168,12 +2696,6 @@ }, "Administration" : { "localizations" : { - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Administración" - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -3208,12 +2730,6 @@ }, "Administration Enabled" : { "localizations" : { - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Administración habilitada" - } - }, "ja" : { "stringUnit" : { "state" : "translated", @@ -3236,12 +2752,6 @@ }, "Advanced" : { "localizations" : { - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Avanzado" - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -3276,12 +2786,6 @@ }, "Advanced Device GPS" : { "localizations" : { - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Dispositivo GPS avanzado" - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -3317,12 +2821,6 @@ "Advanced GPIO Options" : { "extractionState" : "stale", "localizations" : { - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Opciones GPIO avanzadas" - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -3357,12 +2855,6 @@ }, "Advanced Position Flags" : { "localizations" : { - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Banderas de posición avanzadas" - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -3403,12 +2895,6 @@ "value" : "Nach" } }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Después" - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -3479,12 +2965,6 @@ } } }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Después de %lld días" - } - }, "ja" : { "variations" : { "plural" : { @@ -3531,12 +3011,6 @@ "value" : "Nach dem Ändern der Einstellungen wird das Gerät neu starten." } }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Después de guardar los valores de configuración, el nodo se reiniciará." - } - }, "fr" : { "stringUnit" : { "state" : "translated", @@ -3601,12 +3075,6 @@ "value" : "Nachmittag" } }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Tarde" - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -3641,12 +3109,6 @@ "value" : "Airtime" } }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Tiempo en antena" - } - }, "fr" : { "stringUnit" : { "state" : "translated", @@ -3705,12 +3167,6 @@ }, "Alert" : { "localizations" : { - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Alerta" - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -3739,12 +3195,6 @@ }, "Alert GPIO buzzer when receiving a bell" : { "localizations" : { - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Alerta al zumbador GPIO al recibir una campana" - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -3779,12 +3229,6 @@ }, "Alert GPIO buzzer when receiving a message" : { "localizations" : { - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Alerta al zumbador GPIO al recibir un mensaje" - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -3813,12 +3257,6 @@ }, "Alert GPIO vibra motor when receiving a bell" : { "localizations" : { - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Alertar al motor de vibración GPIO al recibir una campana" - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -3853,12 +3291,6 @@ }, "Alert GPIO vibra motor when receiving a message" : { "localizations" : { - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Alertar al motor de vibración GPIO al recibir un mensaje" - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -3887,12 +3319,6 @@ }, "Alert when receiving a bell" : { "localizations" : { - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Alerta al recibir una campana" - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -3927,12 +3353,6 @@ }, "Alert when receiving a message" : { "localizations" : { - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Alerta al recibir un mensaje" - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -3967,12 +3387,6 @@ "value" : "Alle" } }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Todo" - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -4007,12 +3421,6 @@ }, "Allow Position Requests" : { "localizations" : { - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Permitir solicitudes de posición" - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -4041,12 +3449,6 @@ }, "Alt" : { "localizations" : { - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Alt." - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -4081,12 +3483,6 @@ "value" : "Höhe" } }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Altitud" - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -4121,12 +3517,6 @@ "value" : "Höhe %@" } }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Altitud %@" - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -4155,12 +3545,6 @@ }, "Altitude Geoidal Separation" : { "localizations" : { - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Separación geoideal de altitud" - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -4189,12 +3573,6 @@ }, "Altitude is Mean Sea Level" : { "localizations" : { - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "La altitud es el nivel medio del mar." - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -4229,12 +3607,6 @@ "value" : "Immer an" } }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Siempre encendido" - } - }, "fr" : { "stringUnit" : { "state" : "translated", @@ -4299,12 +3671,6 @@ "value" : "Immer nach Norden zeigen" } }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Apunta siempre al norte" - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -4339,12 +3705,6 @@ "value" : "Ambientebeleuchtung" } }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Iluminación ambiental" - } - }, "fr" : { "stringUnit" : { "state" : "translated", @@ -4403,12 +3763,6 @@ "value" : "Ambientebeleuchtungskonfiguration" } }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Configuración de iluminación ambiental" - } - }, "fr" : { "stringUnit" : { "state" : "translated", @@ -4461,12 +3815,6 @@ }, "Ambient Lighting module config received: %@" : { "localizations" : { - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Configuración del módulo de iluminación ambiental recibida: %@" - } - }, "fr" : { "stringUnit" : { "state" : "translated", @@ -4531,12 +3879,6 @@ "value" : "Ein quelloffenes, netzunabhängiges, dezentrales Mesh-Netzwerk, das auf kostengünstigen, stromsparenden Funkgeräten läuft." } }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Una red de malla descentralizada, fuera de la red y de código abierto que funciona con radios asequibles y de bajo consumo." - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -4569,29 +3911,8 @@ } } }, -<<<<<<< Updated upstream -======= - "Anonymous Usage and Crash data" : { - "comment" : "A description of how the app collects and uses data about its usage and crashes. It emphasizes that this data is anonymous and non-personally identifiable.", - "isCommentAutoGenerated" : true, - "localizations" : { - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Datos anónimos de uso y fallos" - } - } - } - }, ->>>>>>> Stashed changes "Any missed messages will be delivered again." : { "localizations" : { - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Cualquier mensaje perdido se entregará nuevamente." - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -4632,12 +3953,6 @@ "value" : "Client (Standard) - Mit App verbundener Client." } }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Aplicación conectada o dispositivo de mensajería independiente." - } - }, "fr" : { "stringUnit" : { "state" : "translated", @@ -4702,12 +4017,6 @@ "value" : "App-Daten" } }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Datos de la aplicación" - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -4742,12 +4051,6 @@ }, "App Files" : { "localizations" : { - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Archivos de aplicación" - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -4782,12 +4085,6 @@ }, "App Icon" : { "localizations" : { - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Icono de aplicación" - } - }, "sr" : { "stringUnit" : { "state" : "translated", @@ -4804,12 +4101,6 @@ "value" : "Mitteilungseinstellungen" } }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Notificaciones de aplicaciones" - } - }, "sr" : { "stringUnit" : { "state" : "translated", @@ -4826,12 +4117,6 @@ "value" : "App-Einstellungen" } }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Configuración de la aplicación" - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -4866,12 +4151,6 @@ }, "Apple Apps" : { "localizations" : { - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Aplicaciones de Apple" - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -4912,12 +4191,6 @@ "value" : "Ungefährer Standort" } }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Ubicación aproximada" - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -4946,12 +4219,6 @@ }, "Are you sure you want to delete this message?" : { "localizations" : { - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "¿Estás seguro de que deseas eliminar este mensaje?" - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -4992,12 +4259,6 @@ "value" : "Bist du sicher dass du den Knoten auf die Werkseinstellungen zurücksetzen willst?" } }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "¿Está seguro de que desea restablecer el nodo a los valores de fábrica?" - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -5038,12 +4299,6 @@ "value" : "Bist Du sicher?" } }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "¿Está seguro?" - } - }, "fr" : { "stringUnit" : { "state" : "translated", @@ -5102,12 +4357,6 @@ }, "Australia / New Zealand" : { "localizations" : { - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Australia / Nueva Zelanda" - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -5136,12 +4385,6 @@ }, "Automatically Connect" : { "localizations" : { - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Conectar automáticamente" - } - }, "sr" : { "stringUnit" : { "state" : "translated", @@ -5152,12 +4395,6 @@ }, "Automatically toggles to the next page on the screen like a carousel, based the specified interval." : { "localizations" : { - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Cambia automáticamente a la página siguiente en la pantalla como un carrusel, según el intervalo especificado." - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -5198,12 +4435,6 @@ "value" : "Verfügbare Modem-Voreinstellungen, Standard ist „Long Range - Fast“." } }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Presets de módem disponibles, el valor predeterminado es Long Fast." - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -5244,12 +4475,6 @@ "value" : "Geräte in der Nähe" } }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Radios disponibles" - } - }, "fr" : { "stringUnit" : { "state" : "translated", @@ -5314,12 +4539,6 @@ "value" : "Zurück" } }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Atrás" - } - }, "fr" : { "stringUnit" : { "state" : "translated", @@ -5378,12 +4597,6 @@ }, "Backup" : { "localizations" : { - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Respaldo" - } - }, "ja" : { "stringUnit" : { "state" : "translated", @@ -5400,12 +4613,6 @@ }, "Backup your private key to your iCloud keychain." : { "localizations" : { - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Haga una copia de seguridad de su clave privada en su llavero de iCloud." - } - }, "ja" : { "stringUnit" : { "state" : "translated", @@ -5422,12 +4629,6 @@ }, "Bad" : { "localizations" : { - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Malo" - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -5456,12 +4657,6 @@ }, "Bad Request" : { "localizations" : { - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Solicitud incorrecta" - } - }, "fr" : { "stringUnit" : { "state" : "translated", @@ -5526,12 +4721,6 @@ "value" : "Bandbreite" } }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Ancho de banda" - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -5566,12 +4755,6 @@ }, "Bar" : { "localizations" : { - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Bar" - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -5606,12 +4789,6 @@ }, "Bar Series" : { "localizations" : { - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Serie de barras" - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -5646,12 +4823,6 @@ }, "Barometric Pressure" : { "localizations" : { - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Presión barométrica" - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -5692,12 +4863,6 @@ "value" : "Batterie" } }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Batería" - } - }, "he" : { "stringUnit" : { "state" : "translated", @@ -5757,12 +4922,6 @@ "value" : "Batterie Ladung" } }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Nivel de batería" - } - }, "fr" : { "stringUnit" : { "state" : "translated", @@ -5828,12 +4987,6 @@ "value" : "Batterie Ladung %" } }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "% del nivel de batería" - } - }, "fr" : { "stringUnit" : { "state" : "translated", @@ -5899,12 +5052,6 @@ "value" : "Batterie Ladung %d" } }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Nivel de batería %d" - } - }, "fr" : { "stringUnit" : { "state" : "translated", @@ -5963,12 +5110,6 @@ }, "Baud" : { "localizations" : { - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "baudios" - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -6015,12 +5156,6 @@ "value" : "Biken" } }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Ciclismo" - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -6055,12 +5190,6 @@ }, "BLE" : { "localizations" : { - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "BLE" - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -6101,12 +5230,6 @@ "value" : "Die Bluetooth Pin muss 6 Stellen lang sein." } }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "El pin BLE debe tener 6 dígitos." - } - }, "fr" : { "stringUnit" : { "state" : "translated", @@ -6171,12 +5294,6 @@ "value" : "Bluetooth" } }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "bluetooth" - } - }, "fr" : { "stringUnit" : { "state" : "translated", @@ -6241,12 +5358,6 @@ "value" : "Bluetooth Konfiguration" } }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Configuración de Bluetooth" - } - }, "fr" : { "stringUnit" : { "state" : "translated", @@ -6311,12 +5422,6 @@ "value" : "Bluetooth Konfiguration empfangen: %@" } }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Configuración de Bluetooth recibida: %@" - } - }, "fr" : { "stringUnit" : { "state" : "translated", @@ -6377,12 +5482,6 @@ "comment" : "A heading displayed on a view that guides users to configure Bluetooth connectivity for the app.", "isCommentAutoGenerated" : true, "localizations" : { - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Conectividad Bluetooth" - } - }, "sr" : { "stringUnit" : { "state" : "translated", @@ -6393,12 +5492,6 @@ }, "Bold Heading" : { "localizations" : { - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Encabezado en negrita" - } - }, "sr" : { "stringUnit" : { "state" : "translated", @@ -6409,12 +5502,6 @@ }, "Bold the heading text on the screen." : { "localizations" : { - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Negrita el texto del encabezado en la pantalla." - } - }, "sr" : { "stringUnit" : { "state" : "translated", @@ -6424,23 +5511,10 @@ } }, "Broadcast Device Metrics" : { - "localizations" : { - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Métricas de dispositivos de transmisión" - } - } - } + }, "Broadcast Interval" : { "localizations" : { - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Intervalo de transmisión" - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -6481,12 +5555,6 @@ "value" : "Sendet GPS-Positionspakete mit Priorität." } }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Transmite paquetes de posición GPS como prioridad." - } - }, "fr" : { "stringUnit" : { "state" : "translated", @@ -6551,12 +5619,6 @@ "value" : "Sendet den Standort regelmäßig als Nachricht an den Standardkanal, um die Suche nach dem Gerät zu unterstützen." } }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Transmite la ubicación como mensaje al canal predeterminado con regularidad para ayudar con la recuperación del dispositivo." - } - }, "fr" : { "stringUnit" : { "state" : "translated", @@ -6621,12 +5683,6 @@ "value" : "Sendet Telemetriepakete mit Priorität." } }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Transmite paquetes de telemetría como prioridad." - } - }, "fr" : { "stringUnit" : { "state" : "translated", @@ -6685,12 +5741,6 @@ }, "Button GPIO" : { "localizations" : { - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Botón GPIO" - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -6725,12 +5775,6 @@ }, "Buy Complete Radios" : { "localizations" : { - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Comprar Radios Completas" - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -6765,12 +5809,6 @@ }, "Buzzer GPIO" : { "localizations" : { - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Zumbador GPIO" - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -6805,12 +5843,6 @@ }, "By enabling this feature, you acknowledge and expressly consent to the transmission of your device’s real-time geographic location over the MQTT protocol without encryption. This location data may be used for purposes such as live map reporting, device tracking, and related telemetry functions." : { "localizations" : { - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Al habilitar esta función, usted reconoce y acepta expresamente la transmisión de la ubicación geográfica en tiempo real de su dispositivo a través del protocolo MQTT sin cifrado. Estos datos de ubicación se pueden utilizar para fines tales como informes de mapas en vivo, seguimiento de dispositivos y funciones de telemetría relacionadas." - } - }, "ja" : { "stringUnit" : { "state" : "translated", @@ -6839,12 +5871,6 @@ "value" : "Bytes" } }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "bytes" - } - }, "fr" : { "stringUnit" : { "state" : "translated", @@ -6904,12 +5930,6 @@ "Bytes Used" : { "comment" : "VoiceOver value for bytes used", "localizations" : { - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Bytes utilizados" - } - }, "ja" : { "stringUnit" : { "state" : "translated", @@ -6932,12 +5952,6 @@ "value" : "Rufzeichen" } }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Señal de llamada" - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -6978,12 +5992,6 @@ "value" : "Das Rufzeichen darf nicht leer sein." } }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "El indicativo de llamada no debe estar vacío" - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -7024,12 +6032,6 @@ "value" : "Abbrechen" } }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Cancelar" - } - }, "fr" : { "stringUnit" : { "state" : "translated", @@ -7088,12 +6090,6 @@ }, "Canned Message module config received: %@" : { "localizations" : { - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Configuración del módulo de mensajes predefinidos recibida: %@" - } - }, "fr" : { "stringUnit" : { "state" : "translated", @@ -7158,12 +6154,6 @@ "value" : "Canned Messages" } }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Mensajes enlatados" - } - }, "fr" : { "stringUnit" : { "state" : "translated", @@ -7228,12 +6218,6 @@ "value" : "Canned Messages Config" } }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Configuración de mensajes predefinidos" - } - }, "fr" : { "stringUnit" : { "state" : "translated", @@ -7292,12 +6276,6 @@ }, "Canned Messages Messages Received For: %@" : { "localizations" : { - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Mensajes almacenados Mensajes recibidos para: %@" - } - }, "fr" : { "stringUnit" : { "state" : "translated", @@ -7356,12 +6334,6 @@ }, "Carousel Interval" : { "localizations" : { - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Intervalo de carrusel" - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -7396,12 +6368,6 @@ "value" : "Kategorien" } }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Categorías" - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -7442,12 +6408,6 @@ "value" : "Kategorie" } }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Categoría" - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -7476,12 +6436,6 @@ }, "Ch1 Current" : { "localizations" : { - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Corriente Ch1" - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -7510,12 +6464,6 @@ }, "Ch1 Voltage" : { "localizations" : { - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Voltaje Ch1" - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -7544,12 +6492,6 @@ }, "Ch2 Current" : { "localizations" : { - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Corriente Ch2" - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -7578,12 +6520,6 @@ }, "Ch2 Voltage" : { "localizations" : { - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Voltaje Ch2" - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -7612,12 +6548,6 @@ }, "Ch3 Current" : { "localizations" : { - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Ch3 actual" - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -7646,12 +6576,6 @@ }, "Ch3 Voltage" : { "localizations" : { - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Voltaje Ch3" - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -7686,12 +6610,6 @@ "value" : "Kanal" } }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Canal" - } - }, "fr" : { "stringUnit" : { "state" : "translated", @@ -7750,12 +6668,6 @@ }, "Channel 0 Included" : { "localizations" : { - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Canal 0 incluido" - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -7790,12 +6702,6 @@ }, "Channel 1" : { "localizations" : { - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Canal 1" - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -7824,12 +6730,6 @@ }, "Channel 1 Included" : { "localizations" : { - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Canal 1 incluido" - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -7864,12 +6764,6 @@ }, "Channel 2" : { "localizations" : { - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Canal 2" - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -7898,12 +6792,6 @@ }, "Channel 2 Included" : { "localizations" : { - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Canal 2 incluido" - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -7938,12 +6826,6 @@ }, "Channel 3" : { "localizations" : { - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Canal 3" - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -7972,12 +6854,6 @@ }, "Channel 3 Included" : { "localizations" : { - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Canal 3 incluido" - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -8012,12 +6888,6 @@ }, "Channel 4 Included" : { "localizations" : { - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Canal 4 incluido" - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -8052,12 +6922,6 @@ }, "Channel 5 Included" : { "localizations" : { - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Canal 5 incluido" - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -8092,12 +6956,6 @@ }, "Channel 6 Included" : { "localizations" : { - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Canal 6 incluido" - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -8132,12 +6990,6 @@ }, "Channel 7 Included" : { "localizations" : { - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Canal 7 incluido" - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -8172,12 +7024,6 @@ }, "Channel Details" : { "localizations" : { - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Detalles del canal" - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -8212,12 +7058,6 @@ }, "Channel Name" : { "localizations" : { - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Nombre del canal" - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -8252,12 +7092,6 @@ }, "Channel number must be between 0 and 7." : { "localizations" : { - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "El número de canal debe estar entre 0 y 7." - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -8292,12 +7126,6 @@ }, "Channel Role" : { "localizations" : { - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Rol del canal" - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -8332,12 +7160,6 @@ }, "Channel URL" : { "localizations" : { - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "URL del canal" - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -8372,12 +7194,6 @@ "value" : "Kanalbelegung" } }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Utilización del canal" - } - }, "fr" : { "stringUnit" : { "state" : "translated", @@ -8436,12 +7252,6 @@ }, "Channel Utilization %@%%" : { "localizations" : { - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Utilización del canal %@%%" - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -8476,12 +7286,6 @@ "value" : "Kanäle" } }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Canales" - } - }, "fr" : { "stringUnit" : { "state" : "translated", @@ -8540,12 +7344,6 @@ }, "Channels being added from the QR code did not save. When adding channels the names must be unique." : { "localizations" : { - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Los canales que se agregaron desde el código QR no se guardaron. Al agregar canales, los nombres deben ser únicos." - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -8580,12 +7378,6 @@ }, "Channels Help" : { "localizations" : { - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Ayuda de canales" - } - }, "ja" : { "stringUnit" : { "state" : "translated", @@ -8602,12 +7394,6 @@ }, "Chart" : { "localizations" : { - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Cuadro" - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -8642,12 +7428,6 @@ }, "CHG" : { "localizations" : { - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "CHG" - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -8676,12 +7456,6 @@ }, "China" : { "localizations" : { - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Porcelana" - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -8710,12 +7484,6 @@ }, "Chirpy" : { "localizations" : { - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Alegre" - } - }, "sr" : { "stringUnit" : { "state" : "translated", @@ -8726,12 +7494,6 @@ }, "Clear" : { "localizations" : { - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Claro" - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -8772,12 +7534,6 @@ "value" : "App-Daten löschen" } }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Borrar datos de la aplicación" - } - }, "fr" : { "stringUnit" : { "state" : "translated", @@ -8836,12 +7592,6 @@ }, "Clear Log" : { "localizations" : { - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Borrar registro" - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -8882,12 +7632,6 @@ "value" : "Veraltete Knoten löschen" } }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Borrar nodos obsoletos" - } - }, "ja" : { "stringUnit" : { "state" : "translated", @@ -8904,12 +7648,6 @@ }, "Client" : { "localizations" : { - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Cliente" - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -8954,12 +7692,6 @@ "value" : "Client - Versteckt" } }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Cliente oculto" - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -8994,12 +7726,6 @@ }, "Client History" : { "localizations" : { - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Historial del cliente" - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -9034,12 +7760,6 @@ }, "Client History Request Sent" : { "localizations" : { - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Solicitud de historial del cliente enviada" - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -9074,12 +7794,6 @@ }, "Client Mute" : { "localizations" : { - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Silencio del cliente" - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -9114,12 +7828,6 @@ }, "Client options" : { "localizations" : { - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Opciones del cliente" - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -9154,12 +7862,6 @@ }, "Clockwise Rotary Event" : { "localizations" : { - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Evento giratorio en el sentido de las agujas del reloj" - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -9200,12 +7902,6 @@ "value" : "Schließen" } }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Cerca" - } - }, "fr" : { "stringUnit" : { "state" : "translated", @@ -9264,12 +7960,6 @@ }, "Coding Rate" : { "localizations" : { - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Tasa de codificación" - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -9310,12 +8000,6 @@ "value" : "Farbe" } }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Color" - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -9356,12 +8040,6 @@ "value" : "Bleibe mit deinen Freunden und deiner Community in Verbindung, auch abseits vom Mobilfunknetz." } }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Comuníquese fuera de la red con sus amigos y su comunidad sin servicio celular." - } - }, "sr" : { "stringUnit" : { "state" : "translated", @@ -9372,12 +8050,6 @@ }, "Communicating" : { "localizations" : { - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Comunicado" - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -9406,12 +8078,6 @@ }, "Community Support" : { "localizations" : { - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Apoyo comunitario" - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -9443,12 +8109,6 @@ }, "Config" : { "localizations" : { - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "configuración" - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -9489,12 +8149,6 @@ "value" : "Konfiguration für: %@" } }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Configuración para: %@" - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -9523,12 +8177,6 @@ }, "Configuration Presets" : { "localizations" : { - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Preajustes de configuración" - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -9569,12 +8217,6 @@ "value" : "Konfigurieren" } }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Configurar" - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -9605,12 +8247,6 @@ "comment" : "Button label to guide users to configure Bluetooth connectivity for the app.", "isCommentAutoGenerated" : true, "localizations" : { - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Configurar la conectividad Bluetooth" - } - }, "sr" : { "stringUnit" : { "state" : "translated", @@ -9623,12 +8259,6 @@ "comment" : "Button label to configure local network access permissions.", "isCommentAutoGenerated" : true, "localizations" : { - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Configurar el acceso a la red local" - } - }, "sr" : { "stringUnit" : { "state" : "translated", @@ -9645,12 +8275,6 @@ "value" : "Standortberechtigungen konfigurieren" } }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Configurar permisos de ubicación" - } - }, "sr" : { "stringUnit" : { "state" : "translated", @@ -9667,12 +8291,6 @@ "value" : "Mitteilungsberechtigungen konfigurieren" } }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Configurar permisos de notificación" - } - }, "sr" : { "stringUnit" : { "state" : "translated", @@ -9683,12 +8301,6 @@ }, "Confirm" : { "localizations" : { - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Confirmar" - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -9717,12 +8329,6 @@ }, "Connect" : { "localizations" : { - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Conectar" - } - }, "sr" : { "stringUnit" : { "state" : "translated", @@ -9745,12 +8351,6 @@ "value" : "Verbunden mit einem Knoten" } }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Conectarse a un nodo" - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -9779,12 +8379,6 @@ }, "Connect to MQTT via Proxy" : { "localizations" : { - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Conéctese a MQTT a través de Proxy" - } - }, "ja" : { "stringUnit" : { "state" : "translated", @@ -9807,12 +8401,6 @@ }, "Connect to new radio?" : { "localizations" : { - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "¿Conectar a una nueva radio?" - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -9847,12 +8435,6 @@ "value" : "Derzeit verbunden" } }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Conectado" - } - }, "fr" : { "stringUnit" : { "state" : "translated", @@ -9917,12 +8499,6 @@ "value" : "Verbunden mit Knoten %@" } }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Nodo conectado %@" - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -9951,12 +8527,6 @@ }, "Connected Radio" : { "localizations" : { - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Radio conectada" - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -9991,12 +8561,6 @@ "value" : "Verbinde..." } }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Conectando. ." - } - }, "fr" : { "stringUnit" : { "state" : "translated", @@ -10055,12 +8619,6 @@ }, "Connecting to a new radio will clear all app data on the phone." : { "localizations" : { - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Al conectarse a una nueva radio se borrarán todos los datos de la aplicación en el teléfono." - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -10095,12 +8653,6 @@ "value" : "Verbindungsversuch %lld von 10" } }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Intento de conexión %lld de 10" - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -10135,12 +8687,6 @@ }, "Connection Name" : { "localizations" : { - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Nombre de conexión" - } - }, "sr" : { "stringUnit" : { "state" : "translated", @@ -10151,12 +8697,6 @@ }, "Consent to Share Unencrypted Node Data via MQTT" : { "localizations" : { - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Consentimiento para compartir datos de nodos no cifrados a través de MQTT" - } - }, "ja" : { "stringUnit" : { "state" : "translated", @@ -10185,12 +8725,6 @@ "value" : "Kontaktfilter" } }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Filtros de contacto" - } - }, "sr" : { "stringUnit" : { "state" : "translated", @@ -10201,12 +8735,6 @@ }, "Contact URL" : { "localizations" : { - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "URL de contacto" - } - }, "ja" : { "stringUnit" : { "state" : "translated", @@ -10235,12 +8763,6 @@ "value" : "Kontakte (%@)" } }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Contactos (%@)" - } - }, "fr" : { "stringUnit" : { "state" : "translated", @@ -10299,12 +8821,6 @@ }, "Control Type" : { "localizations" : { - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Tipo de control" - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -10339,12 +8855,6 @@ }, "Controls the blinking LED on the device. For most devices this will control one of the up to 4 LEDS, the charger and GPS LEDs are not controllable." : { "localizations" : { - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Controla el LED parpadeante en el dispositivo. Para la mayoría de los dispositivos, esto controlará uno de los hasta 4 LED, los LED del cargador y del GPS no son controlables." - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -10385,12 +8895,6 @@ "value" : "Konvexe Hülle" } }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Casco convexo" - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -10425,12 +8929,6 @@ "value" : "Koordinate" } }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Coordinar" - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -10465,12 +8963,6 @@ "value" : "Koordinate %1$@, %2$@" } }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Coordinar %@, %@" - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -10511,12 +9003,6 @@ "value" : "Koordinaten:" } }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Coordenadas:" - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -10551,12 +9037,6 @@ "value" : "Kopieren" } }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Copiar" - } - }, "fr" : { "stringUnit" : { "state" : "translated", @@ -10621,12 +9101,6 @@ "value" : "Knoten nicht gefunden" } }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "No se pudo encontrar el nodo" - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -10661,12 +9135,6 @@ }, "Counter Clockwise Rotary Event" : { "localizations" : { - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Evento giratorio en sentido antihorario" - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -10705,12 +9173,6 @@ "value" : "Wegpunkt erstellen" } }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Crear punto de referencia" - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -10745,12 +9207,6 @@ "value" : "Erstelle deine eigenen Netzwerke" } }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Crea tus propias redes" - } - }, "sr" : { "stringUnit" : { "state" : "translated", @@ -10767,12 +9223,6 @@ "value" : "Erstellt: %@" } }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Creado: %@" - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -10807,12 +9257,6 @@ "value" : "Kritische Hinweise" } }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Alertas críticas" - } - }, "sr" : { "stringUnit" : { "state" : "translated", @@ -10823,12 +9267,6 @@ }, "Current" : { "localizations" : { - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Actual" - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -10863,12 +9301,6 @@ "value" : "Aktuelle Firmware Version: %@" } }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Versión de firmware actual: %@" - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -10909,12 +9341,6 @@ "value" : "Aktuelle Firmware Version: %1$@, neuste Firmware Version %2$@" } }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Versión de firmware actual: %@, Última versión de firmware: %@" - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -10955,12 +9381,6 @@ "value" : "Aktuell: %lld" } }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Actual: %lld" - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -10989,12 +9409,6 @@ }, "Currently the recommended way to update ESP32 devices is using the web flasher on a desktop computer from a chrome based browser. It does not work on mobile devices or over BLE." : { "localizations" : { - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Actualmente, la forma recomendada de actualizar dispositivos ESP32 es utilizar el flash web en una computadora de escritorio desde un navegador basado en Chrome. No funciona en dispositivos móviles ni a través de BLE." - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -11035,12 +9449,6 @@ "value" : "Datum" } }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Fecha" - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -11069,12 +9477,6 @@ }, "Debug" : { "localizations" : { - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Depurar" - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -11115,12 +9517,6 @@ "value" : "Fehlersuchprotokolle" } }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Registros de depuración" - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -11149,12 +9545,6 @@ }, "Debug Logs%@" : { "localizations" : { - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Registros de depuración%@" - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -11189,12 +9579,6 @@ "value" : "Standard" } }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Por defecto" - } - }, "fr" : { "stringUnit" : { "state" : "translated", @@ -11253,12 +9637,6 @@ }, "Default 128x64 screen layout" : { "localizations" : { - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Diseño de pantalla predeterminado de 128x64" - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -11299,12 +9677,6 @@ "value" : "Löschen" } }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Borrar" - } - }, "fr" : { "stringUnit" : { "state" : "translated", @@ -11363,12 +9735,6 @@ }, "Delete all config, keys and BLE bonds? " : { "localizations" : { - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "¿Eliminar todas las configuraciones, claves y enlaces BLE?" - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -11391,12 +9757,6 @@ }, "Delete all config? " : { "localizations" : { - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "¿Eliminar todas las configuraciones?" - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -11419,12 +9779,6 @@ }, "Delete all device metrics?" : { "localizations" : { - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "¿Eliminar todas las métricas del dispositivo?" - } - }, "fr" : { "stringUnit" : { "state" : "translated", @@ -11483,12 +9837,6 @@ }, "Delete all environment metrics?" : { "localizations" : { - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "¿Eliminar todas las métricas del entorno?" - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -11523,12 +9871,6 @@ }, "Delete all pax data?" : { "localizations" : { - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "¿Eliminar todos los datos de los pasajeros?" - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -11563,12 +9905,6 @@ }, "Delete all positions?" : { "localizations" : { - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "¿Eliminar todas las posiciones?" - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -11597,12 +9933,6 @@ }, "Delete Message" : { "localizations" : { - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Eliminar mensaje" - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -11631,12 +9961,6 @@ }, "Delete Messages" : { "localizations" : { - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Eliminar mensajes" - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -11671,12 +9995,6 @@ "value" : "Knoten löschen" } }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Eliminar nodo" - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -11711,12 +10029,6 @@ "value" : "Knoten löschen?" } }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "¿Eliminar nodo?" - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -11745,12 +10057,6 @@ }, "Delete Power metrics?" : { "localizations" : { - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "¿Eliminar métricas de potencia?" - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -11785,12 +10091,6 @@ "value" : "Beschreibung" } }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Descripción" - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -11819,12 +10119,6 @@ }, "Description must be less than 100 bytes" : { "localizations" : { - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "La descripción debe tener menos de 100 bytes." - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -11859,12 +10153,6 @@ }, "Details..." : { "localizations" : { - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Detalles..." - } - }, "sr" : { "stringUnit" : { "state" : "translated", @@ -11875,12 +10163,6 @@ }, "Detection" : { "localizations" : { - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Detección" - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -11909,12 +10191,6 @@ }, "Detection event" : { "localizations" : { - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Evento de detección" - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -11950,12 +10226,6 @@ "value" : "Detection Sensor" } }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Sensor de detección" - } - }, "fr" : { "stringUnit" : { "state" : "translated", @@ -12014,12 +10284,6 @@ }, "Detection Sensor Config" : { "localizations" : { - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Configuración del sensor de detección" - } - }, "fr" : { "stringUnit" : { "state" : "translated", @@ -12072,12 +10336,6 @@ }, "Detection Sensor Log" : { "localizations" : { - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Registro del sensor de detección" - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -12112,12 +10370,6 @@ }, "Detection sensor messages are received as text messages. If you enable notifications you will recieve a notification for each detection message received and a corresponding unread message badge." : { "localizations" : { - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Los mensajes del sensor de detección se reciben como mensajes de texto. Si habilita las notificaciones, recibirá una notificación por cada mensaje de detección recibido y la correspondiente insignia de mensaje no leído." - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -12152,12 +10404,6 @@ }, "Detection Sensor module config received: %@" : { "localizations" : { - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Configuración del módulo del sensor de detección recibida: %@" - } - }, "fr" : { "stringUnit" : { "state" : "translated", @@ -12216,12 +10462,6 @@ }, "Developers" : { "localizations" : { - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Desarrolladores" - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -12262,12 +10502,6 @@ "value" : "Gerät" } }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Dispositivo" - } - }, "fr" : { "stringUnit" : { "state" : "translated", @@ -12332,12 +10566,6 @@ "value" : "Gerätekonfiguration" } }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Configuración del dispositivo" - } - }, "fr" : { "stringUnit" : { "state" : "translated", @@ -12402,12 +10630,6 @@ "value" : "Gerätekonfiguration empfangen: %@" } }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Configuración del dispositivo recibida: %@" - } - }, "fr" : { "stringUnit" : { "state" : "translated", @@ -12472,12 +10694,6 @@ "value" : "Gerätekonfiguration" } }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Configuración del dispositivo" - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -12524,12 +10740,6 @@ "value" : "Geräte-GPS" } }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Dispositivo GPS" - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -12564,12 +10774,6 @@ }, "Device is managed by a mesh administrator, the user is unable to access any of the device settings." : { "localizations" : { - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "El dispositivo es administrado por un administrador de malla, el usuario no puede acceder a ninguna de las configuraciones del dispositivo." - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -12610,12 +10814,6 @@ "value" : "Device Metadata empfangen von: %@" } }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Metadatos del dispositivo recibidos de: %@" - } - }, "fr" : { "stringUnit" : { "state" : "translated", @@ -12674,12 +10872,6 @@ }, "Device Metrics" : { "localizations" : { - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Métricas del dispositivo" - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -12714,12 +10906,6 @@ }, "Device Metrics Log" : { "localizations" : { - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Registro de métricas del dispositivo" - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -12760,12 +10946,6 @@ "value" : "Gerätemodell: %@" } }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Modelo de dispositivo: %@" - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -12799,23 +10979,10 @@ } }, "Device Options" : { - "localizations" : { - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Opciones del dispositivo" - } - } - } + }, "Device Role" : { "localizations" : { - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Función del dispositivo" - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -12850,12 +11017,6 @@ }, "Device Screen" : { "localizations" : { - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Pantalla del dispositivo" - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -12896,12 +11057,6 @@ "value" : "Gerät, das keine Pakete von anderen Geräten weiterleitet." } }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Dispositivo que no reenvía paquetes desde otros dispositivos." - } - }, "fr" : { "stringUnit" : { "state" : "translated", @@ -12966,12 +11121,6 @@ "value" : "Gerät, das nur bei Bedarf sendet, um nicht entdeckt zu werden oder Strom zu sparen." } }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Dispositivo que solo transmite según sea necesario para sigilo o ahorro de energía." - } - }, "fr" : { "stringUnit" : { "state" : "translated", @@ -13030,12 +11179,6 @@ }, "Dilution of precision (DOP) PDOP used by default" : { "localizations" : { - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Dilución de precisión (DOP) PDOP utilizado por defecto" - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -13070,12 +11213,6 @@ "value" : "Direkt" } }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Directo" - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -13116,12 +11253,6 @@ "value" : "Hilfe für Direktnachrichten" } }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Ayuda por mensaje directo" - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -13156,12 +11287,6 @@ }, "Direct Message Key" : { "localizations" : { - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Tecla de mensaje directo" - } - }, "ja" : { "stringUnit" : { "state" : "translated", @@ -13184,12 +11309,6 @@ "value" : "Direktnachrichten" } }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Mensajes directos" - } - }, "fr" : { "stringUnit" : { "state" : "translated", @@ -13248,12 +11367,6 @@ }, "Direct messages are using the new public key infrastructure for encryption. Requires firmware version 2.5 or greater." : { "localizations" : { - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Los mensajes directos utilizan la nueva infraestructura de clave pública para el cifrado. Requiere versión de firmware 2.5 o superior." - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -13288,12 +11401,6 @@ }, "Direct messages are using the shared key for the channel." : { "localizations" : { - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Los mensajes directos utilizan la clave compartida del canal." - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -13334,12 +11441,6 @@ "value" : "Deaktiviert" } }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Desactivado" - } - }, "fr" : { "stringUnit" : { "state" : "translated", @@ -13404,12 +11505,6 @@ "value" : "Trennen" } }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Desconectar" - } - }, "fr" : { "stringUnit" : { "state" : "translated", @@ -13468,12 +11563,6 @@ }, "Disconnect Node" : { "localizations" : { - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Desconectar nodo" - } - }, "ja" : { "stringUnit" : { "state" : "translated", @@ -13490,12 +11579,6 @@ }, "Disconnect the currently connected node" : { "localizations" : { - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Desconectar el nodo actualmente conectado" - } - }, "ja" : { "stringUnit" : { "state" : "translated", @@ -13518,12 +11601,6 @@ "value" : "Tastatur ausblenden" } }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Despedir" - } - }, "fr" : { "stringUnit" : { "state" : "translated", @@ -13588,12 +11665,6 @@ "value" : "Display (Device Screen)" } }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Mostrar" - } - }, "fr" : { "stringUnit" : { "state" : "translated", @@ -13652,12 +11723,6 @@ }, "Display Config" : { "localizations" : { - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Configuración de pantalla" - } - }, "fr" : { "stringUnit" : { "state" : "translated", @@ -13722,12 +11787,6 @@ "value" : "Display Konfiguration empfangen: %@" } }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Configuración de pantalla recibida: %@" - } - }, "fr" : { "stringUnit" : { "state" : "translated", @@ -13786,12 +11845,6 @@ }, "Display Fahrenheit" : { "localizations" : { - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Mostrar grados Fahrenheit" - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -13826,12 +11879,6 @@ }, "Display Mode" : { "localizations" : { - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Modo de visualización" - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -13872,12 +11919,6 @@ "value" : "Darstellung der Entfernung zwischen deinem Handy und anderen Meshtastic-Knoten mit Positionsangabe." } }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Muestra la distancia entre tu teléfono y otros nodos Meshtastic con posiciones." - } - }, "sr" : { "stringUnit" : { "state" : "translated", @@ -13888,12 +11929,6 @@ }, "Display Units" : { "localizations" : { - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Unidades de visualización" - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -13934,12 +11969,6 @@ "value" : "Distanz" } }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Distancia" - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -13980,12 +12009,6 @@ "value" : "Distanzfilter" } }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Filtros de distancia" - } - }, "sr" : { "stringUnit" : { "state" : "translated", @@ -14002,12 +12025,6 @@ "value" : "Distanzmessungen" } }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Mediciones de distancia" - } - }, "sr" : { "stringUnit" : { "state" : "translated", @@ -14027,12 +12044,6 @@ "value" : "Dokumentation" } }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Documentación" - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -14067,12 +12078,6 @@ }, "Done" : { "localizations" : { - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Hecho" - } - }, "ja" : { "stringUnit" : { "state" : "translated", @@ -14095,12 +12100,6 @@ }, "Double Tap as Button" : { "localizations" : { - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Toque dos veces como botón" - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -14141,12 +12140,6 @@ "value" : "Runter" } }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Abajo" - } - }, "fr" : { "stringUnit" : { "state" : "translated", @@ -14205,12 +12198,6 @@ }, "Downlink Enabled" : { "localizations" : { - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Enlace descendente habilitado" - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -14245,12 +12232,6 @@ }, "Drag & Drop Firmware Update" : { "localizations" : { - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Actualización de firmware de arrastrar y soltar" - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -14285,12 +12266,6 @@ }, "Drag & Drop Firmware Update Documentation" : { "localizations" : { - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Documentación de actualización de firmware de arrastrar y soltar" - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -14325,12 +12300,6 @@ }, "Drag & Drop is the recommended way to update firmware for NRF devices. If your iPhone or iPad is USB-C it will work with your regular USB-C charging cable, for lightning devices you need the Apple Lightning to USB camera adaptor." : { "localizations" : { - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Arrastrar y soltar es la forma recomendada de actualizar el firmware para dispositivos NRF. Si su iPhone o iPad es USB-C, funcionará con su cable de carga USB-C habitual; para dispositivos Lightning, necesita el adaptador de cámara Lightning a USB de Apple." - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -14371,12 +12340,6 @@ "value" : "Fahren" } }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Conduciendo" - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -14411,12 +12374,6 @@ }, "Drop Pin in Maps" : { "localizations" : { - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Colocar pin en mapas" - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -14451,12 +12408,6 @@ "value" : "Richte einfach private Mesh-Netzwerke für eine sichere und zuverlässige Kommunikation in abgelegenen Gebieten ein." } }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Configure fácilmente redes de malla privadas para una comunicación segura y confiable en áreas remotas." - } - }, "sr" : { "stringUnit" : { "state" : "translated", @@ -14467,12 +12418,6 @@ }, "Echo" : { "localizations" : { - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Eco" - } - }, "fr" : { "stringUnit" : { "state" : "translated", @@ -14525,12 +12470,6 @@ }, "Editing Waypoint" : { "localizations" : { - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Editar punto de referencia" - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -14565,12 +12504,6 @@ "value" : "Achtzehn Stunden" } }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Dieciocho horas" - } - }, "fr" : { "stringUnit" : { "state" : "translated", @@ -14635,12 +12568,6 @@ "value" : "Höhenunterschied" } }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Elev. Ganar" - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -14669,12 +12596,6 @@ }, "Emoji" : { "localizations" : { - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "emojis" - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -14709,12 +12630,6 @@ }, "Empty" : { "localizations" : { - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Vacío" - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -14748,23 +12663,10 @@ } }, "Enable broadcasting device metrics to the mesh network. When disabled, metrics are only sent to connected clients." : { - "localizations" : { - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Habilite la transmisión de métricas de dispositivos a la red de malla. Cuando está deshabilitado, las métricas solo se envían a los clientes conectados." - } - } - } + }, "Enable broadcasting packets via UDP over the local network." : { "localizations" : { - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Habilite la transmisión de paquetes a través de UDP a través de la red local." - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -14799,12 +12701,6 @@ "value" : "Standortfreigabe aktivieren" } }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Habilitar compartir ubicación" - } - }, "sr" : { "stringUnit" : { "state" : "translated", @@ -14815,12 +12711,6 @@ }, "Enable Notifications" : { "localizations" : { - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Habilitar notificaciones" - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -14855,12 +12745,6 @@ }, "Enable this device as a Store and Forward server. Requires an ESP32 device with PSRAM." : { "localizations" : { - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Habilite este dispositivo como servidor Store and Forward. Requiere un dispositivo ESP32 con PSRAM." - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -14895,12 +12779,6 @@ "value" : "Aktiviert" } }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Activado" - } - }, "fr" : { "stringUnit" : { "state" : "translated", @@ -14965,12 +12843,6 @@ "value" : "Aktiviert automatische TAK-PLI-Übertragungen und verringert die Anzahl der Routineübertragungen." } }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Permite transmisiones automáticas de TAK PLI y reduce las transmisiones de rutina." - } - }, "fr" : { "stringUnit" : { "state" : "translated", @@ -15029,12 +12901,6 @@ }, "Enables devices with native I2S audio output to use the RTTTL over speaker like a buzzer. T-Watch S3 and T-Deck for example have this capability." : { "localizations" : { - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Permite que los dispositivos con salida de audio I2S nativa utilicen el RTTTL a través del altavoz como un timbre. T-Watch S3 y T-Deck, por ejemplo, tienen esta capacidad." - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -15075,12 +12941,6 @@ "value" : "Aktiviert den blauen Standort-Punkt für dein Handy in der Mesh-Karte." } }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Habilita el punto de ubicación azul para su teléfono en el mapa de malla." - } - }, "sr" : { "stringUnit" : { "state" : "translated", @@ -15091,12 +12951,6 @@ }, "Enables the detection sensor module, it needs to be enabled on both the node with the sensor, and any nodes that you want to receive detection sensor text messages or view the detection sensor log and chart." : { "localizations" : { - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Habilita el módulo del sensor de detección; debe estar habilitado tanto en el nodo con el sensor como en cualquier nodo en el que desee recibir mensajes de texto del sensor de detección o ver el registro y el gráfico del sensor de detección." - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -15131,12 +12985,6 @@ }, "Enables the store and forward module." : { "localizations" : { - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Habilita el módulo de almacenamiento y reenvío." - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -15165,12 +13013,6 @@ }, "Enabling Ethernet will disable the bluetooth connection to the app." : { "localizations" : { - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Al habilitar Ethernet se deshabilitará la conexión bluetooth a la aplicación." - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -15193,12 +13035,6 @@ }, "Enabling WiFi will disable the bluetooth connection to the app." : { "localizations" : { - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Habilitar WiFi deshabilitará la conexión bluetooth a la aplicación." - } - }, "sr" : { "stringUnit" : { "state" : "translated", @@ -15209,12 +13045,6 @@ }, "Encoder Press Event" : { "localizations" : { - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Evento de prensa del codificador" - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -15249,12 +13079,6 @@ "value" : "Verschlüsselt" } }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "cifrado" - } - }, "fr" : { "stringUnit" : { "state" : "translated", @@ -15320,12 +13144,6 @@ "value" : "Verschlüsseltes Senden fehlgeschlagen" } }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Error de envío cifrado" - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -15354,12 +13172,6 @@ }, "Encryption Enabled" : { "localizations" : { - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Cifrado habilitado" - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -15400,12 +13212,6 @@ "value" : "DFÜ-Modus aktivieren" } }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Ingrese al modo DFU" - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -15442,12 +13248,6 @@ "comment" : "A label for a text field where the user can enter a hostname or IP address and optionally a port number.", "isCommentAutoGenerated" : true, "localizations" : { - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Introduzca el nombre de host[:puerto]" - } - }, "sr" : { "stringUnit" : { "state" : "translated", @@ -15464,12 +13264,6 @@ "value" : "Umgebung" } }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "ambiente" - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -15510,12 +13304,6 @@ "value" : "Umgebung" } }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Ambiente" - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -15550,12 +13338,6 @@ }, "Environment Metrics" : { "localizations" : { - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Métricas ambientales" - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -15589,23 +13371,10 @@ } }, "Environment Metrics Enabled" : { - "localizations" : { - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Métricas de entorno habilitadas" - } - } - } + }, "Environment Metrics Log" : { "localizations" : { - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Registro de métricas ambientales" - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -15639,14 +13408,7 @@ } }, "Environment Sensor Options" : { - "localizations" : { - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Opciones de sensores ambientales" - } - } - } + }, "Erase all app data?" : { "localizations" : { @@ -15656,12 +13418,6 @@ "value" : "Alle App-Daten löschen?" } }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "¿Borrar todos los datos de la aplicación?" - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -15702,12 +13458,6 @@ "value" : "Alle Geräte- und App-Daten löschen?" } }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "¿Borrar todos los datos del dispositivo y de las aplicaciones?" - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -15742,12 +13492,6 @@ }, "Error: %@" : { "localizations" : { - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Error: %@" - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -15782,12 +13526,6 @@ }, "ESP 32 OTA update is a work in progress, click the button below to send your device a reboot into ota admin message." : { "localizations" : { - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "La actualización de ESP 32 OTA es un trabajo en progreso, haga clic en el botón a continuación para enviar su dispositivo a un reinicio en el mensaje de administrador de ota." - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -15822,12 +13560,6 @@ }, "ESP32 Device Firmware Update" : { "localizations" : { - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Actualización del firmware del dispositivo ESP32" - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -15862,12 +13594,6 @@ }, "Ethernet Options" : { "localizations" : { - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Opciones de Ethernet" - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -15896,12 +13622,6 @@ }, "European Union 433MHz" : { "localizations" : { - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Unión Europea 433MHz" - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -15930,12 +13650,6 @@ }, "European Union 868MHz" : { "localizations" : { - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Unión Europea 868MHz" - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -15970,12 +13684,6 @@ "value" : "Abend" } }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Noche" - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -16004,12 +13712,6 @@ }, "Exchange Positions" : { "localizations" : { - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Posiciones de intercambio" - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -16044,12 +13746,6 @@ "value" : "Ausrufezeichen" } }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Exclamación" - } - }, "fr" : { "stringUnit" : { "state" : "translated", @@ -16108,12 +13804,6 @@ }, "Expiration" : { "localizations" : { - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Vencimiento" - } - }, "ja" : { "stringUnit" : { "state" : "translated", @@ -16136,12 +13826,6 @@ "value" : "Zeitpunkt" } }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Expirar" - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -16176,12 +13860,6 @@ "value" : "Automatisches Löschen" } }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Vence" - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -16210,12 +13888,6 @@ }, "Expires: %@" : { "localizations" : { - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Vence: %@" - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -16250,12 +13922,6 @@ "value" : "Exportieren" } }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Exportar" - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -16290,12 +13956,6 @@ "value" : "Externe Benachrichtigung" } }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Notificación externa" - } - }, "fr" : { "stringUnit" : { "state" : "translated", @@ -16360,12 +14020,6 @@ "value" : "Einstellungen der externen Benachrichtigung" } }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Configuración de notificación externa" - } - }, "fr" : { "stringUnit" : { "state" : "translated", @@ -16424,12 +14078,6 @@ }, "External Notification module config received: %@" : { "localizations" : { - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Configuración del módulo de notificación externa recibida: %@" - } - }, "fr" : { "stringUnit" : { "state" : "translated", @@ -16494,12 +14142,6 @@ "value" : "Werkseinstellungen" } }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Restablecimiento de fábrica" - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -16528,12 +14170,6 @@ }, "Factory reset will delete device and app data." : { "localizations" : { - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "El restablecimiento de fábrica eliminará los datos del dispositivo y de la aplicación." - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -16556,12 +14192,6 @@ }, "Failed to encode message content" : { "localizations" : { - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "No se pudo codificar el contenido del mensaje" - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -16590,12 +14220,6 @@ }, "Failed to get a valid position to exchange" : { "localizations" : { - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "No se pudo obtener una posición válida para intercambiar" - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -16624,12 +14248,6 @@ }, "Failed to get a valid position to exchange." : { "localizations" : { - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "No se pudo obtener una posición válida para intercambiar." - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -16664,12 +14282,6 @@ "value" : "Ordentliche Signalstärke" } }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Justo" - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -16704,12 +14316,6 @@ "value" : "Favorit" } }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Favorito" - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -16745,12 +14351,6 @@ "value" : "Knoten, die als Favorit markiert oder ignoriert wurden, bleiben immer erhalten. Knoten ohne PKC-Schlüssel werden gemäß dem festgelegten Zeitplan aus der App-Datenbank gelöscht. Knoten mit PKC-Schlüsseln werden nur gelöscht, wenn das Intervall auf 7 Tage oder länger eingestellt ist. Diese Funktion löscht nur Knoten aus der App, die nicht in der Geräteknoten-Datenbank gespeichert sind." } }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Los nodos favoritos e ignorados siempre se conservan. Los nodos sin claves PKC se borran de la base de datos de la aplicación según el cronograma establecido por el usuario, los nodos con claves PKC se borran solo si el intervalo se establece en 7 días o más. Esta función solo elimina los nodos de la aplicación que no están almacenados en la base de datos de nodos del dispositivo." - } - }, "ja" : { "stringUnit" : { "state" : "translated", @@ -16776,12 +14376,6 @@ "value" : "Favoriten" } }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Favoritos" - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -16816,12 +14410,6 @@ "value" : "Favoriten und Knoten mit aktuellen Nachrichten werden oben in der Kontaktliste angezeigt." } }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Los favoritos y los nodos con mensajes recientes aparecen en la parte superior de la lista de contactos." - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -16862,12 +14450,6 @@ "value" : "Letzte Position eines Knotens holen" } }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Obtener la última posición de un nodo determinado" - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -16896,12 +14478,6 @@ }, "Fifteen Minutes" : { "localizations" : { - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "quince minutos" - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -16936,12 +14512,6 @@ "value" : "Fünfzehn Sekunden" } }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "quince segundos" - } - }, "fr" : { "stringUnit" : { "state" : "translated", @@ -17000,12 +14570,6 @@ }, "File Storage" : { "localizations" : { - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Almacenamiento de archivos" - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -17035,12 +14599,6 @@ "Files Available" : { "comment" : "Data source label when files exist but none are active", "localizations" : { - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Archivos disponibles" - } - }, "sr" : { "stringUnit" : { "state" : "translated", @@ -17057,12 +14615,6 @@ "value" : "Filtere die Knotenliste und die Mesh-Karte nach der Nähe zu deinem Handy." } }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Filtre la lista de nodos y el mapa de malla según la proximidad a su teléfono." - } - }, "sr" : { "stringUnit" : { "state" : "translated", @@ -17079,12 +14631,6 @@ "value" : "Kontakt suchen" } }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "encontrar un contacto" - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -17125,12 +14671,6 @@ "value" : "Einen Knoten finden" } }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Encuentra un nodo" - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -17171,12 +14711,6 @@ "value" : "Beenden" } }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Finalizar" - } - }, "fr" : { "stringUnit" : { "state" : "translated", @@ -17242,12 +14776,6 @@ "value" : "Ziel" } }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Finalizar" - } - }, "sr" : { "stringUnit" : { "state" : "translated", @@ -17264,12 +14792,6 @@ "value" : "Firmware" } }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "firmware" - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -17304,12 +14826,6 @@ }, "Firmware update docs" : { "localizations" : { - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Documentos de actualización de firmware" - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -17350,12 +14866,6 @@ "value" : "Firmwareaktualisierungen" } }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Actualizaciones de firmware" - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -17396,12 +14906,6 @@ "value" : "Firmware Version" } }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Versión de firmware" - } - }, "fr" : { "stringUnit" : { "state" : "translated", @@ -17460,12 +14964,6 @@ }, "First heard" : { "localizations" : { - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "escuchado por primera vez" - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -17506,12 +15004,6 @@ "value" : "Fünf Stunden" } }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "cinco horas" - } - }, "fr" : { "stringUnit" : { "state" : "translated", @@ -17576,12 +15068,6 @@ "value" : "Fünf Minuten" } }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "cinco minutos" - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -17616,12 +15102,6 @@ "value" : "Fünf Sekunden" } }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "cinco segundos" - } - }, "fr" : { "stringUnit" : { "state" : "translated", @@ -17686,12 +15166,6 @@ "value" : "Feste PIN" } }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Pasador fijo" - } - }, "fr" : { "stringUnit" : { "state" : "translated", @@ -17750,12 +15224,6 @@ }, "Fixed Position" : { "localizations" : { - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Posición fija" - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -17784,12 +15252,6 @@ }, "Flip Screen" : { "localizations" : { - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Voltear pantalla" - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -17818,12 +15280,6 @@ }, "Flip screen vertically" : { "localizations" : { - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Voltear la pantalla verticalmente" - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -17858,12 +15314,6 @@ "value" : "Folgen" } }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Seguir" - } - }, "fr" : { "stringUnit" : { "state" : "translated", @@ -17928,12 +15378,6 @@ "value" : "Folgen mit Steuerkurs" } }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Seguir con encabezado" - } - }, "fr" : { "stringUnit" : { "state" : "translated", @@ -17992,12 +15436,6 @@ }, "For all Mqtt functionality other than the map report you must also set uplink and downlink for each channel you want to bridge over Mqtt." : { "localizations" : { - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Para todas las funciones de Mqtt además del informe de mapa, también debe configurar el enlace ascendente y descendente para cada canal que desee conectar a través de Mqtt." - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -18038,12 +15476,6 @@ "value" : "Für alle" } }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Para todos" - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -18078,12 +15510,6 @@ "value" : "Für mich" } }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Para mí" - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -18118,12 +15544,6 @@ "value" : "Achtundvierzig Stunden" } }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "cuarenta y ocho horas" - } - }, "fr" : { "stringUnit" : { "state" : "translated", @@ -18188,12 +15608,6 @@ "value" : "Fündundvierzig Sekunden" } }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "cuarenta y cinco segundos" - } - }, "fr" : { "stringUnit" : { "state" : "translated", @@ -18258,12 +15672,6 @@ "value" : "Vier Stunden" } }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "cuatro horas" - } - }, "fr" : { "stringUnit" : { "state" : "translated", @@ -18328,12 +15736,6 @@ "value" : "Vier Sekunden" } }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "cuatro segundos" - } - }, "fr" : { "stringUnit" : { "state" : "translated", @@ -18398,12 +15800,6 @@ "value" : "Frequenz" } }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Frecuencia" - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -18432,12 +15828,6 @@ }, "Frequency Override" : { "localizations" : { - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Anulación de frecuencia" - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -18466,12 +15856,6 @@ }, "Frequency Slot" : { "localizations" : { - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Ranura de frecuencia" - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -18500,12 +15884,6 @@ }, "Friendly name" : { "localizations" : { - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Nombre amigable" - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -18540,12 +15918,6 @@ }, "Friendly name used to format message sent to mesh. Example: A name \"Motion\" would result in a message \"Motion detected\"" : { "localizations" : { - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Nombre descriptivo utilizado para formatear el mensaje enviado a la malla. Ejemplo: un nombre \"Movimiento\" daría como resultado un mensaje \"Movimiento detectado\"." - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -18580,12 +15952,6 @@ }, "From Radio (RX): %lld" : { "localizations" : { - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Desde radio (RX): %lld" - } - }, "sr" : { "stringUnit" : { "state" : "translated", @@ -18596,12 +15962,6 @@ }, "Full Support" : { "localizations" : { - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Soporte completo" - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -18636,12 +15996,6 @@ }, "Generate a new private key to replace the one currently in use. The public key will automatically be regenerated from your private key." : { "localizations" : { - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Genere una nueva clave privada para reemplazar la que está actualmente en uso. La clave pública se regenerará automáticamente a partir de su clave privada." - } - }, "ja" : { "stringUnit" : { "state" : "translated", @@ -18664,12 +16018,6 @@ "value" : "QR Code Erzeugen" } }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Generar código QR" - } - }, "fr" : { "stringUnit" : { "state" : "translated", @@ -18728,12 +16076,6 @@ }, "Get custom waterproof solar and detection sensor router nodes, aluminium desktop nodes and rugged handsets." : { "localizations" : { - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Obtenga nodos de enrutador de sensores de detección y solares impermeables personalizados, nodos de escritorio de aluminio y teléfonos resistentes." - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -18768,12 +16110,6 @@ "value" : "Knotenposition ermitteln" } }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Obtener la posición del nodo" - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -18808,12 +16144,6 @@ }, "Get NRF DFU from the App Store" : { "localizations" : { - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Obtenga NRF DFU en la App Store" - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -18854,12 +16184,6 @@ "value" : "Los geht's" } }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "empezar" - } - }, "sr" : { "stringUnit" : { "state" : "translated", @@ -18870,12 +16194,6 @@ }, "Get the latest stable firmware" : { "localizations" : { - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Obtenga el firmware estable más reciente" - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -18910,12 +16228,6 @@ }, "GitHub Repository" : { "localizations" : { - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Repositorio GitHub" - } - }, "ja" : { "stringUnit" : { "state" : "translated", @@ -18932,12 +16244,6 @@ }, "Good" : { "localizations" : { - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Bien" - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -18966,12 +16272,6 @@ }, "GPIO" : { "localizations" : { - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "GPIO" - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -19006,12 +16306,6 @@ }, "GPIO Output Duration" : { "localizations" : { - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Duración de la salida GPIO" - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -19040,12 +16334,6 @@ }, "GPIO pin for rotary encoder A port." : { "localizations" : { - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Pin GPIO para el puerto A del codificador rotatorio." - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -19074,12 +16362,6 @@ }, "GPIO pin for rotary encoder B port." : { "localizations" : { - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Pin GPIO para el puerto B del codificador rotatorio." - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -19108,12 +16390,6 @@ }, "GPIO pin for rotary encoder Press port." : { "localizations" : { - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Pin GPIO para codificador rotatorio Puerto de prensa." - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -19142,12 +16418,6 @@ }, "GPIO Pin to monitor" : { "localizations" : { - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Pin GPIO para monitorear" - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -19176,12 +16446,6 @@ }, "GPS EN GPIO" : { "localizations" : { - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "GPS EN GPIO" - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -19216,12 +16480,6 @@ }, "GPS Receive GPIO" : { "localizations" : { - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Recepción GPS GPIO" - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -19256,12 +16514,6 @@ }, "GPS Transmit GPIO" : { "localizations" : { - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Transmisión GPS GPIO" - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -19302,12 +16554,6 @@ "value" : "Gruppennachricht" } }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Mensaje grupal" - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -19342,12 +16588,6 @@ }, "Gusts %@" : { "localizations" : { - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Ráfagas %@" - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -19376,12 +16616,6 @@ }, "HaHa" : { "localizations" : { - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Ja ja" - } - }, "he" : { "stringUnit" : { "state" : "translated", @@ -19422,12 +16656,6 @@ }, "Hard Reset" : { "localizations" : { - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Restablecimiento completo" - } - }, "sr" : { "stringUnit" : { "state" : "translated", @@ -19438,12 +16666,6 @@ }, "Hardware" : { "localizations" : { - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Hardware" - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -19478,12 +16700,6 @@ }, "Hazardous" : { "localizations" : { - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Peligroso" - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -19512,12 +16728,6 @@ }, "Heading" : { "localizations" : { - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Título" - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -19552,12 +16762,6 @@ "value" : "Kurs: %@" } }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Título: %@" - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -19592,12 +16796,6 @@ "value" : "Gehört" } }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Escuchó" - } - }, "fr" : { "stringUnit" : { "state" : "translated", @@ -19662,12 +16860,6 @@ "value" : "Herz" } }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Corazón" - } - }, "fr" : { "stringUnit" : { "state" : "translated", @@ -19726,12 +16918,6 @@ }, "Hide alerts" : { "localizations" : { - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Ocultar alertas" - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -19766,12 +16952,6 @@ }, "Hide Alerts" : { "localizations" : { - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Ocultar alertas" - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -19806,12 +16986,6 @@ "value" : "HOCH" } }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "ALTO" - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -19852,12 +17026,6 @@ "value" : "Wandern" } }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Senderismo" - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -19892,12 +17060,6 @@ }, "History Return Max" : { "localizations" : { - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Historial Retorno Max" - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -19926,12 +17088,6 @@ }, "History Return Window" : { "localizations" : { - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Ventana de retorno del historial" - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -19966,12 +17122,6 @@ "value" : "Hops Entfernt" } }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "salta lejos" - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -20006,12 +17156,6 @@ "value" : "Hops Entfernt %d" } }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Salta lejos %d" - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -20046,12 +17190,6 @@ "value" : "Hops Entfernt:" } }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Saltos lejos:" - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -20086,12 +17224,6 @@ "value" : "Hops Entfernt: %d" } }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Saltos lejos: %d" - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -20126,12 +17258,6 @@ "value" : "Stunde" } }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Hora" - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -20166,12 +17292,6 @@ }, "Hourly Duty Cycle" : { "localizations" : { - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Ciclo de trabajo por hora" - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -20200,12 +17320,6 @@ }, "How long the screen remains on after the user button is pressed or messages are received." : { "localizations" : { - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Cuánto tiempo permanece encendida la pantalla después de presionar el botón de usuario o recibir mensajes." - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -20240,12 +17354,6 @@ }, "How often device metrics are sent out over the mesh. Default is 30 minutes." : { "localizations" : { - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Con qué frecuencia se envían las métricas del dispositivo a través de la malla. El valor predeterminado es 30 minutos." - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -20280,12 +17388,6 @@ }, "How often environment metrics are sent out over the mesh. Default is 30 minutes." : { "localizations" : { - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Con qué frecuencia se envían métricas ambientales a través de la malla. El valor predeterminado es 30 minutos." - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -20320,12 +17422,6 @@ }, "How often power metrics are sent out over the mesh. Default is 30 minutes." : { "localizations" : { - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Con qué frecuencia se envían métricas de potencia a través de la malla. El valor predeterminado es 30 minutos." - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -20360,12 +17456,6 @@ }, "How often should we try to get a GPS position." : { "localizations" : { - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "¿Con qué frecuencia debemos intentar obtener una posición GPS?" - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -20400,12 +17490,6 @@ }, "How often to send detection sensor state to mesh regardless of detection. Default is Never." : { "localizations" : { - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Con qué frecuencia enviar el estado del sensor de detección a la malla independientemente de la detección. El valor predeterminado es Nunca." - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -20446,12 +17530,6 @@ "value" : "How often we can send a message to the mesh when people are detected." } }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Con qué frecuencia podemos enviar un mensaje a la malla cuando se detectan personas." - } - }, "he" : { "stringUnit" : { "state" : "translated", @@ -20510,12 +17588,6 @@ "value" : "Wie wird die Firmware aktualisiert" } }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Cómo actualizar el firmware" - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -20550,12 +17622,6 @@ }, "Hum" : { "localizations" : { - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Tararear" - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -20590,12 +17656,6 @@ "value" : "Luftfeuchtigkeit" } }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Humedad" - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -20624,12 +17684,6 @@ }, "Hybrid" : { "localizations" : { - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Híbrido" - } - }, "fr" : { "stringUnit" : { "state" : "translated", @@ -20688,12 +17742,6 @@ }, "Hybrid Flyover" : { "localizations" : { - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Paso elevado híbrido" - } - }, "fr" : { "stringUnit" : { "state" : "translated", @@ -20752,12 +17800,6 @@ }, "I have read and understand the above. I voluntarily consent to the unencrypted transmission of my node data via MQTT." : { "localizations" : { - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "He leído y entiendo lo anterior. Doy mi consentimiento voluntariamente para la transmisión sin cifrar de los datos de mi nodo a través de MQTT." - } - }, "ja" : { "stringUnit" : { "state" : "translated", @@ -20780,12 +17822,6 @@ }, "IAQ" : { "localizations" : { - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "IAQ" - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -20820,12 +17856,6 @@ }, "IAQ " : { "localizations" : { - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "IAQ" - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -20860,12 +17890,6 @@ }, "IAQ %lld" : { "localizations" : { - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "IAQ %lld" - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -20906,12 +17930,6 @@ "value" : "Emoji" } }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Icono" - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -20946,12 +17964,6 @@ }, "Icons" : { "localizations" : { - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Iconos" - } - }, "sr" : { "stringUnit" : { "state" : "translated", @@ -20962,12 +17974,6 @@ }, "If DOP is set, use HDOP / VDOP values instead of PDOP" : { "localizations" : { - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Si se configura DOP, use valores HDOP/VDOP en lugar de PDOP" - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -21002,12 +18008,6 @@ }, "If enabled, the 'output' Pin will be pulled active high, disabled means active low." : { "localizations" : { - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Si está habilitado, el pin de 'salida' se activará alto, deshabilitado significa activo bajo." - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -21042,12 +18042,6 @@ }, "If it is hard to access your device's reset button enter DFU mode here." : { "localizations" : { - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Si le resulta difícil acceder al botón de reinicio de su dispositivo, ingrese al modo DFU aquí." - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -21082,12 +18076,6 @@ }, "If set, any packets you send will be echoed back to your device." : { "localizations" : { - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Si está configurado, cualquier paquete que envíe se enviará a su dispositivo." - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -21122,12 +18110,6 @@ }, "If the default region topic is too busy you can choose a more local topic." : { "localizations" : { - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Si el tema de la región predeterminada está demasiado ocupado, puede elegir un tema más local." - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -21162,12 +18144,6 @@ }, "Ignore MQTT" : { "localizations" : { - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Ignorar MQTT" - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -21202,12 +18178,6 @@ }, "Ignore Node" : { "localizations" : { - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Ignorar nodo" - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -21242,12 +18212,6 @@ }, "Ignored" : { "localizations" : { - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "ignorado" - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -21282,12 +18246,6 @@ }, "Ignores observed messages from foreign meshes like Local Only, but takes it step further by also ignoring messages from nodes not already in the node's known list." : { "localizations" : { - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Ignora los mensajes observados de mallas externas como Solo local, pero va un paso más allá al ignorar también los mensajes de nodos que aún no están en la lista conocida del nodo." - } - }, "ja" : { "stringUnit" : { "state" : "translated", @@ -21310,12 +18268,6 @@ }, "Ignores observed messages from foreign meshes that are open or those which it cannot decrypt. Only rebroadcasts message on the nodes local primary / secondary channels." : { "localizations" : { - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Ignora los mensajes observados de mallas externas que están abiertas o aquellas que no puede descifrar. Sólo retransmite mensajes en los canales primarios/secundarios locales del nodo." - } - }, "ja" : { "stringUnit" : { "state" : "translated", @@ -21344,12 +18296,6 @@ "value" : "Route importieren" } }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Ruta de importación" - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -21384,12 +18330,6 @@ }, "In addition to Config, Keys and BLE bonds will be wiped" : { "localizations" : { - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Además de la configuración, se borrarán las claves y los enlaces BLE." - } - }, "sr" : { "stringUnit" : { "state" : "translated", @@ -21406,12 +18346,6 @@ "value" : "Include" } }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Incluir" - } - }, "fr" : { "stringUnit" : { "state" : "translated", @@ -21476,12 +18410,6 @@ "value" : "Eingehende Nachrichten" } }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Mensajes entrantes" - } - }, "sr" : { "stringUnit" : { "state" : "translated", @@ -21498,12 +18426,6 @@ "value" : "Unvollständig" } }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Incompleto" - } - }, "fr" : { "stringUnit" : { "state" : "translated", @@ -21538,12 +18460,6 @@ }, "India" : { "localizations" : { - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "India" - } - }, "ja" : { "stringUnit" : { "state" : "translated", @@ -21566,12 +18482,6 @@ }, "Indoor Air Quality" : { "localizations" : { - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Calidad del aire interior" - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -21606,12 +18516,6 @@ }, "Indoor Air Quality (IAQ)" : { "localizations" : { - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Calidad del aire interior (IAQ)" - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -21652,12 +18556,6 @@ "value" : "Router - Mesh Pakete werden bevorzugt über diesen Knoten gerouted. Dieser Knoten wird nicht von einer Client App benutzt. WLAN, Bluetooth und Display sind aus." } }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Nodo de infraestructura únicamente en una torre o cima de una montaña. No debe usarse para techos o nodos móviles. Necesita una cobertura excepcional. Visible en la lista de nodos." - } - }, "fr" : { "stringUnit" : { "state" : "translated", @@ -21716,12 +18614,6 @@ }, "Inputs" : { "localizations" : { - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Entradas" - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -21762,12 +18654,6 @@ "value" : "Ungültiger Dateiinhalt. Bitte überprüfe das Dateiformat." } }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Contenido del archivo no válido. Por favor verifique el formato del archivo." - } - }, "sr" : { "stringUnit" : { "state" : "translated", @@ -21778,12 +18664,6 @@ }, "Inverted top bar for 2 Color display" : { "localizations" : { - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Barra superior invertida para pantalla de 2 colores" - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -21818,12 +18698,6 @@ }, "Japan" : { "localizations" : { - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Japón" - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -21852,12 +18726,6 @@ }, "JSON Enabled" : { "localizations" : { - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "JSON habilitado" - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -21892,12 +18760,6 @@ }, "JSON mode is a limited, unencrypted MQTT output for locally integrating with home assistant" : { "localizations" : { - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "El modo JSON es una salida MQTT limitada y sin cifrar para la integración local con el asistente doméstico" - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -21932,12 +18794,6 @@ }, "Jump to present" : { "localizations" : { - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Saltar al presente" - } - }, "ja" : { "stringUnit" : { "state" : "translated", @@ -21966,12 +18822,6 @@ "value" : "Schlüssel" } }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Llave" - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -22006,12 +18856,6 @@ }, "Key Backup" : { "localizations" : { - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Copia de seguridad clave" - } - }, "ja" : { "stringUnit" : { "state" : "translated", @@ -22028,12 +18872,6 @@ }, "Key Mapping" : { "localizations" : { - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Mapeo de claves" - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -22068,12 +18906,6 @@ "value" : "Schlüsselgröße" } }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Tamaño de clave" - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -22102,12 +18934,6 @@ }, "Korea" : { "localizations" : { - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Corea" - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -22142,12 +18968,6 @@ "value" : "Zuletzt gehört" } }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Escuchado por última vez" - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -22195,12 +19015,6 @@ "value" : "Breitengrad" } }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Latitud" - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -22235,12 +19049,6 @@ }, "Latitude in degrees (e.g., 37.7749)" : { "localizations" : { - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Latitud en grados (por ejemplo, 37,7749)" - } - }, "ja" : { "stringUnit" : { "state" : "translated", @@ -22257,12 +19065,6 @@ }, "Latitude must be between -90 and 90 degrees" : { "localizations" : { - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "La latitud debe estar entre -90 y 90 grados." - } - }, "ja" : { "stringUnit" : { "state" : "translated", @@ -22279,12 +19081,6 @@ }, "LED Heartbeat" : { "localizations" : { - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Latido del corazón LED" - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -22325,12 +19121,6 @@ "value" : "LED Status" } }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Estado del LED" - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -22371,12 +19161,6 @@ "value" : "Links" } }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Izquierda" - } - }, "fr" : { "stringUnit" : { "state" : "translated", @@ -22441,12 +19225,6 @@ "value" : "Level" } }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Nivel" - } - }, "fr" : { "stringUnit" : { "state" : "translated", @@ -22505,12 +19283,6 @@ }, "Licensed Operator" : { "localizations" : { - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Operador Licenciado" - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -22545,12 +19317,6 @@ }, "Limit all periodic broadcast intervals especially telemetry and position. If you need to increase hops, do it on nodes at the edges, not the ones in the middle. MQTT is not advised when you are duty cycle restricted because the gateway node is then doing all the work." : { "localizations" : { - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Limite todos los intervalos de transmisión periódica, especialmente la telemetría y la posición. Si necesita aumentar los saltos, hágalo en los nodos de los bordes, no en los del medio. No se recomienda MQTT cuando el ciclo de trabajo está restringido porque el nodo de puerta de enlace está haciendo todo el trabajo." - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -22585,12 +19351,6 @@ }, "Line Series" : { "localizations" : { - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Serie de línea" - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -22619,12 +19379,6 @@ }, "Loading Logs. . ." : { "localizations" : { - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Cargando registros. . ." - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -22661,12 +19415,6 @@ "comment" : "A label displayed above the options for local network access.", "isCommentAutoGenerated" : true, "localizations" : { - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Acceso a la red local" - } - }, "sr" : { "stringUnit" : { "state" : "translated", @@ -22683,12 +19431,6 @@ "value" : "Standort:" } }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Ubicación:" - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -22729,12 +19471,6 @@ "value" : "Gesperrt" } }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "bloqueado" - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -22769,12 +19505,6 @@ }, "Log Levels" : { "localizations" : { - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Niveles de registro" - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -22815,12 +19545,6 @@ "value" : "Logging" } }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Explotación florestal" - } - }, "fr" : { "stringUnit" : { "state" : "translated", @@ -22879,12 +19603,6 @@ }, "Logs" : { "localizations" : { - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Registros" - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -22919,12 +19637,6 @@ }, "Logs:" : { "localizations" : { - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Registros:" - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -22965,12 +19677,6 @@ "value" : "Langer Name" } }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Nombre largo" - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -23011,12 +19717,6 @@ "value" : "Durch langes Gedrückthalten kannst du den Kontakt zu deinen Favoriten hinzufügen, stumm schalten oder eine Unterhaltung löschen." } }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Mantenga presionado para marcar como favorito, silenciar el contacto o eliminar una conversación." - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -23051,12 +19751,6 @@ }, "Long Range - Fast" : { "localizations" : { - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Largo alcance - Rápido" - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -23085,12 +19779,6 @@ }, "Long Range - Moderate" : { "localizations" : { - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Largo alcance - Moderado" - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -23119,12 +19807,6 @@ }, "Long Range - Slow" : { "localizations" : { - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Largo alcance - Lento" - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -23159,12 +19841,6 @@ "value" : "Längengrad" } }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Longitud" - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -23199,12 +19875,6 @@ }, "Longitude in degrees (e.g., -122.4194)" : { "localizations" : { - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Longitud en grados (p. ej., -122,4194)" - } - }, "ja" : { "stringUnit" : { "state" : "translated", @@ -23221,12 +19891,6 @@ }, "Longitude must be between -180 and 180 degrees" : { "localizations" : { - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "La longitud debe estar entre -180 y 180 grados." - } - }, "ja" : { "stringUnit" : { "state" : "translated", @@ -23249,12 +19913,6 @@ "value" : "LoRa" } }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "lora" - } - }, "fr" : { "stringUnit" : { "state" : "translated", @@ -23319,12 +19977,6 @@ "value" : "LoRa Einstellungen" } }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Configuración LoRa" - } - }, "fr" : { "stringUnit" : { "state" : "translated", @@ -23383,12 +20035,6 @@ }, "LoRa Config Changes:" : { "localizations" : { - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Cambios en la configuración de LoRa:" - } - }, "sr" : { "stringUnit" : { "state" : "translated", @@ -23405,12 +20051,6 @@ "value" : "LoRa config empfangen: %@" } }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Configuración de LoRa recibida: %@" - } - }, "fr" : { "stringUnit" : { "state" : "translated", @@ -23475,12 +20115,6 @@ "value" : "Tracker" } }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Objetos perdidos y encontrados" - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -23515,12 +20149,6 @@ }, "LOW" : { "localizations" : { - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "BAJO" - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -23555,12 +20183,6 @@ "value" : "Niedriger Akkustand" } }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Batería baja" - } - }, "sr" : { "stringUnit" : { "state" : "translated", @@ -23577,12 +20199,6 @@ "value" : "M5 Stack Card KB / RAK Tastenfeld" } }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Tarjeta de pila M5 Teclado KB / RAK" - } - }, "fr" : { "stringUnit" : { "state" : "translated", @@ -23623,12 +20239,6 @@ }, "Malaysia 433MHz" : { "localizations" : { - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Malasia 433MHz" - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -23657,12 +20267,6 @@ }, "Malaysia 919MHz" : { "localizations" : { - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Malasia 919MHz" - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -23697,12 +20301,6 @@ "value" : "Kanäle verwalten" } }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Administrar canales" - } - }, "fr" : { "stringUnit" : { "state" : "translated", @@ -23762,12 +20360,6 @@ "Manage custom map overlays" : { "comment" : "Subtitle for map data management", "localizations" : { - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Administrar superposiciones de mapas personalizados" - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -23809,12 +20401,6 @@ "value" : "Kartendaten verwalten" } }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Administrar datos de mapas" - } - }, "sr" : { "stringUnit" : { "state" : "translated", @@ -23825,12 +20411,6 @@ }, "Managed Device" : { "localizations" : { - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Dispositivo administrado" - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -23865,12 +20445,6 @@ }, "Manual" : { "localizations" : { - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Manual" - } - }, "sr" : { "stringUnit" : { "state" : "translated", @@ -23893,12 +20467,6 @@ "value" : "Manuelle Konfiguration" } }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Configuración manual" - } - }, "fr" : { "stringUnit" : { "state" : "translated", @@ -23957,12 +20525,6 @@ }, "Manual connection string" : { "localizations" : { - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Cadena de conexión manual" - } - }, "sr" : { "stringUnit" : { "state" : "translated", @@ -23976,12 +20538,6 @@ }, "Map Data" : { "localizations" : { - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Datos del mapa" - } - }, "sr" : { "stringUnit" : { "state" : "translated", @@ -23998,12 +20554,6 @@ "value" : "Kartenoptionen" } }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Opciones de mapa" - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -24044,12 +20594,6 @@ "value" : "Karten-Overlays" } }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Superposiciones de mapas" - } - }, "fr" : { "stringUnit" : { "state" : "translated", @@ -24108,12 +20652,6 @@ }, "Map Publish Interval" : { "localizations" : { - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Intervalo de publicación de mapas" - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -24148,12 +20686,6 @@ }, "Map Report" : { "localizations" : { - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Informe de mapa" - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -24194,12 +20726,6 @@ "value" : "Maximale Wiederholungen erreicht" } }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Retransmisión máxima alcanzada" - } - }, "fr" : { "stringUnit" : { "state" : "translated", @@ -24258,12 +20784,6 @@ }, "Medium Range - Fast" : { "localizations" : { - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Rango medio - Rápido" - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -24292,12 +20812,6 @@ }, "Medium Range - Slow" : { "localizations" : { - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Rango medio - Lento" - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -24326,12 +20840,6 @@ }, "Mesh activity update" : { "localizations" : { - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Actualización de actividad de malla" - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -24366,12 +20874,6 @@ "value" : "Mesh Live Aktivität" } }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Actividad en vivo de malla" - } - }, "fr" : { "stringUnit" : { "state" : "translated", @@ -24436,12 +20938,6 @@ "value" : "Mesh Karte" } }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Mapa de malla" - } - }, "fr" : { "stringUnit" : { "state" : "translated", @@ -24506,12 +21002,6 @@ "value" : "Standort auf der Mesh-Karte" } }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Ubicación del mapa de malla" - } - }, "sr" : { "stringUnit" : { "state" : "translated", @@ -24522,12 +21012,6 @@ }, "Meshtastic" : { "localizations" : { - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Meshtástico" - } - }, "sr" : { "stringUnit" : { "state" : "translated", @@ -24547,12 +21031,6 @@ "value" : "Meshtastic Knoten %@ hat Kanäle mit dir geteilt" } }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Meshtastic Node %@ ha compartido canales contigo" - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -24587,12 +21065,6 @@ "value" : "Meshtastic verwendet den Standort deines Handys, um eine Reihe von Funktionen zu ermöglichen. Du kannst deine Standortberechtigungen jederzeit in den Einstellungen aktualisieren." } }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Meshtastic utiliza la ubicación de su teléfono para habilitar una serie de funciones. Puede actualizar sus permisos de ubicación en cualquier momento desde la configuración." - } - }, "sr" : { "stringUnit" : { "state" : "translated", @@ -24603,12 +21075,6 @@ }, "Meshtastic® Copyright Meshtastic LLC" : { "localizations" : { - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Meshtastic® Copyright Meshtastic LLC" - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -24643,12 +21109,6 @@ "value" : "Nachricht" } }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Mensaje" - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -24683,12 +21143,6 @@ "value" : "Nachrichteninhalt überschreitet 200 Bytes." } }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "El contenido del mensaje supera los 200 bytes." - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -24723,12 +21177,6 @@ "value" : "Nachrichtendetails" } }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Detalles del mensaje" - } - }, "fr" : { "stringUnit" : { "state" : "translated", @@ -24793,12 +21241,6 @@ "value" : "Nachricht von der Textnachricht-App empfangen." } }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Mensaje recibido de la aplicación de mensajes de texto." - } - }, "fr" : { "stringUnit" : { "state" : "translated", @@ -24858,12 +21300,6 @@ "Message Size" : { "comment" : "VoiceOver label for message size", "localizations" : { - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Tamaño del mensaje" - } - }, "ja" : { "stringUnit" : { "state" : "translated", @@ -24880,12 +21316,6 @@ }, "Message Status Options" : { "localizations" : { - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Opciones de estado del mensaje" - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -24920,12 +21350,6 @@ "value" : "Nachrichten" } }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Mensajes" - } - }, "he" : { "stringUnit" : { "state" : "translated", @@ -24984,12 +21408,6 @@ "value" : "Nachrichten getrennt mit |" } }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Los mensajes se separan con |" - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -25018,12 +21436,6 @@ }, "Messaging" : { "localizations" : { - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Mensajería" - } - }, "ja" : { "stringUnit" : { "state" : "translated", @@ -25040,12 +21452,6 @@ }, "Metric" : { "localizations" : { - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Métrico" - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -25080,12 +21486,6 @@ "value" : "Mittag" } }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Uno" - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -25120,12 +21520,6 @@ "value" : "Minimum Distanz" } }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Distancia mínima" - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -25160,12 +21554,6 @@ "value" : "Minimum Intervall" } }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Intervalo mínimo" - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -25194,12 +21582,6 @@ }, "Minimum time between detection broadcasts" : { "localizations" : { - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Tiempo mínimo entre transmisiones de detección" - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -25228,12 +21610,6 @@ }, "Mininum time between detection broadcasts. Default is 45 seconds." : { "localizations" : { - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Tiempo mínimo entre transmisiones de detección. El valor predeterminado es 45 segundos." - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -25268,12 +21644,6 @@ "value" : "Modus" } }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Modo" - } - }, "fr" : { "stringUnit" : { "state" : "translated", @@ -25332,12 +21702,6 @@ }, "Model" : { "localizations" : { - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Modelo" - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -25372,12 +21736,6 @@ }, "Moderate" : { "localizations" : { - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Moderado" - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -25418,12 +21776,6 @@ "value" : "Modul Konfiguration" } }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Configuración del módulo" - } - }, "fr" : { "stringUnit" : { "state" : "translated", @@ -25488,12 +21840,6 @@ "value" : "Morgen" } }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Mañana" - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -25528,12 +21874,6 @@ "value" : "Die meisten Daten in deinem Mesh werden über den primären Kanal gesendet. Du kannst sekundäre Kanäle einrichten, um zusätzliche Nachrichtengruppen zu erstellen, die durch ihren eigenen Schlüssel gesichert sind. [Tipps zur Kanalkonfiguration](https://meshtastic.org/docs/configuration/radio/channels/)" } }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "La mayoría de los datos de su malla se envían a través del canal principal. Puede configurar canales secundarios para crear grupos de mensajería adicionales protegidos por su propia clave. [Consejos de configuración de canales](https://meshtastic.org/docs/configuration/tips/)" - } - }, "fr" : { "stringUnit" : { "state" : "translated", @@ -25592,12 +21932,6 @@ }, "MQTT" : { "localizations" : { - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "MQTT" - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -25638,12 +21972,6 @@ "value" : "MQTT Client Proxy" } }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Proxy de cliente MQTT" - } - }, "fr" : { "stringUnit" : { "state" : "translated", @@ -25708,12 +22036,6 @@ "value" : "MQTT Konfiguration" } }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Configuración MQTT" - } - }, "fr" : { "stringUnit" : { "state" : "translated", @@ -25778,12 +22100,6 @@ "value" : "MQTT Modulkonfiguration empfangen: %@" } }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Configuración del módulo MQTT recibida: %@" - } - }, "fr" : { "stringUnit" : { "state" : "translated", @@ -25848,12 +22164,6 @@ "value" : "Multiplier" } }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Multiplicador" - } - }, "he" : { "stringUnit" : { "state" : "translated", @@ -25900,12 +22210,6 @@ }, "Must be a single emoji" : { "localizations" : { - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Debe ser un solo emoji" - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -25940,12 +22244,6 @@ "value" : "MyInfo empfangen: %@" } }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Mi información recibida: %@" - } - }, "fr" : { "stringUnit" : { "state" : "translated", @@ -26005,12 +22303,6 @@ "Nag timeout" : { "extractionState" : "stale", "localizations" : { - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Se acabó el tiempo" - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -26038,14 +22330,7 @@ } }, "Nag Timeout" : { - "localizations" : { - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Tiempo agotado" - } - } - } + }, "Name" : { "localizations" : { @@ -26055,12 +22340,6 @@ "value" : "Name" } }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Nombre" - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -26101,12 +22380,6 @@ "value" : "Name muss kürzer als 30 Bytes sein" } }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "El nombre debe tener menos de 30 bytes." - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -26141,12 +22414,6 @@ }, "Navigate to node" : { "localizations" : { - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Navegar al nodo" - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -26175,12 +22442,6 @@ }, "Nearby Topics" : { "localizations" : { - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Temas cercanos" - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -26215,12 +22476,6 @@ "value" : "Netzwerk" } }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Red" - } - }, "fr" : { "stringUnit" : { "state" : "translated", @@ -26285,12 +22540,6 @@ "value" : "Netzwerkeinstellungen" } }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Configuración de red" - } - }, "fr" : { "stringUnit" : { "state" : "translated", @@ -26355,12 +22604,6 @@ "value" : "Netzwerkkonfiguration empfangen: %@" } }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Configuración de red recibida: %@" - } - }, "fr" : { "stringUnit" : { "state" : "translated", @@ -26419,12 +22662,6 @@ }, "Network Status Orange" : { "localizations" : { - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Estado de la red naranja" - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -26459,12 +22696,6 @@ }, "Network Status Red" : { "localizations" : { - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Estado de la red Rojo" - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -26499,12 +22730,6 @@ }, "New Node" : { "localizations" : { - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Nuevo nodo" - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -26533,12 +22758,6 @@ }, "New Node has been discovered" : { "localizations" : { - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Se ha descubierto un nuevo nodo." - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -26573,12 +22792,6 @@ "value" : "Neue Knoten" } }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Nuevos nodos" - } - }, "sr" : { "stringUnit" : { "state" : "translated", @@ -26589,12 +22802,6 @@ }, "New Zealand 865MHz" : { "localizations" : { - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Nueva Zelanda 865MHz" - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -26629,12 +22836,6 @@ "value" : "Neuere Firmware ist verfügbar" } }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Hay un firmware más nuevo disponible" - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -26675,12 +22876,6 @@ "value" : "Nacht" } }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Noche" - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -26715,12 +22910,6 @@ "value" : "NMEA Positionen" } }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Posiciones NMEA" - } - }, "fr" : { "stringUnit" : { "state" : "translated", @@ -26785,12 +22974,6 @@ "value" : "Kein Kanal" } }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Sin canal" - } - }, "fr" : { "stringUnit" : { "state" : "translated", @@ -26855,12 +23038,6 @@ "value" : "Kein verbundener Knoten" } }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Ningún nodo conectado" - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -26896,12 +23073,6 @@ "value" : "Keine Daten vorhanden" } }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Sin datos" - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -26942,12 +23113,6 @@ "value" : "Kein Gerät verbunden" } }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Ningún dispositivo conectado" - } - }, "fr" : { "stringUnit" : { "state" : "translated", @@ -27006,12 +23171,6 @@ }, "No Device Metrics" : { "localizations" : { - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Sin métricas de dispositivo" - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -27040,12 +23199,6 @@ }, "No Environment Metrics" : { "localizations" : { - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Sin métricas ambientales" - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -27086,12 +23239,6 @@ "value" : "Keine Dateien hochgeladen" } }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "No se subieron archivos" - } - }, "sr" : { "stringUnit" : { "state" : "translated", @@ -27108,12 +23255,6 @@ "value" : "Keine Schnittstelle" } }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Sin interfaz" - } - }, "fr" : { "stringUnit" : { "state" : "translated", @@ -27173,12 +23314,6 @@ "No map data files uploaded" : { "comment" : "Message when no files are uploaded", "localizations" : { - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "No se han subido archivos de datos de mapas" - } - }, "sr" : { "stringUnit" : { "state" : "translated", @@ -27189,12 +23324,6 @@ }, "No PAX Counter Logs" : { "localizations" : { - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Sin registros de contador de PAX" - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -27235,12 +23364,6 @@ "value" : "Keine PIN (geht einfach)" } }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Sin PIN (simplemente funciona)" - } - }, "fr" : { "stringUnit" : { "state" : "translated", @@ -27305,12 +23428,6 @@ "value" : "Keine Positionen" } }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Sin posiciones" - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -27339,12 +23456,6 @@ }, "No Power Metrics" : { "localizations" : { - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Sin métricas de energía" - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -27379,12 +23490,6 @@ "value" : "Keine Antwort" } }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Sin respuesta" - } - }, "fr" : { "stringUnit" : { "state" : "translated", @@ -27449,12 +23554,6 @@ "value" : "Keine Route" } }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Sin ruta" - } - }, "fr" : { "stringUnit" : { "state" : "translated", @@ -27519,12 +23618,6 @@ "value" : "Knoten" } }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Nodo" - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -27559,12 +23652,6 @@ "value" : "Node Core Data Backup %1$@/%2$@ - %3$@ - %4$@" } }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Copia de seguridad de datos principales del nodo %@/%@ - %@ - %@" - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -27605,12 +23692,6 @@ "value" : "Knoten hat keine Position" } }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "El nodo no tiene posiciones" - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -27645,12 +23726,6 @@ "value" : "Knoten Historie" } }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Historia del nodo" - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -27679,12 +23754,6 @@ }, "Node Info Broadcast Interval" : { "localizations" : { - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Intervalo de transmisión de información de nodo" - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -27719,12 +23788,6 @@ "value" : "Knotenkarte" } }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Mapa de nodos" - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -27763,12 +23826,6 @@ "value" : "Knotennummer" } }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Número de nodo" - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -27809,12 +23866,6 @@ "value" : "Knoten" } }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Nodos" - } - }, "fr" : { "stringUnit" : { "state" : "translated", @@ -27873,12 +23924,6 @@ "value" : "Knoten (%@)" } }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Nodos (%@)" - } - }, "fr" : { "stringUnit" : { "state" : "translated", @@ -27943,12 +23988,6 @@ "value" : "Keins" } }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Ninguno" - } - }, "fr" : { "stringUnit" : { "state" : "translated", @@ -28007,12 +24046,6 @@ }, "Not a valid route file" : { "localizations" : { - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "No es un archivo de ruta válido" - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -28047,12 +24080,6 @@ "value" : "Nicht authorisiert" } }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "No autorizado" - } - }, "fr" : { "stringUnit" : { "state" : "translated", @@ -28109,24 +24136,8 @@ } } }, - "Not Connected" : { - "localizations" : { - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "No conectado" - } - } - } - }, "Not Present" : { "localizations" : { - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "No presente" - } - }, "fr" : { "stringUnit" : { "state" : "translated", @@ -28185,12 +24196,6 @@ "value" : "Knoten" } }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Notas" - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -28225,12 +24230,6 @@ "value" : "Mitteilungen für Kanal- und Direktnachrichten." } }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Notificaciones por canal y mensajes directos." - } - }, "sr" : { "stringUnit" : { "state" : "translated", @@ -28247,12 +24246,6 @@ "value" : "Mitteilungen bei niedrigem Akkustand des verbundenen Funkgeräts." } }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Notificaciones de alertas de batería baja para el dispositivo conectado." - } - }, "sr" : { "stringUnit" : { "state" : "translated", @@ -28269,12 +24262,6 @@ "value" : "Mitteilungen für neu entdeckte Knoten." } }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Notificaciones para nodos recién descubiertos." - } - }, "sr" : { "stringUnit" : { "state" : "translated", @@ -28291,12 +24278,6 @@ "value" : "Anzahl Hops" } }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Número de saltos" - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -28331,12 +24312,6 @@ "value" : "Anzahl Einträge" } }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Número de registros" - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -28371,12 +24346,6 @@ "value" : "Anzahl Satelliten" } }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Número de satélites" - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -28405,12 +24374,6 @@ }, "Ok" : { "localizations" : { - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "De acuerdo" - } - }, "sr" : { "stringUnit" : { "state" : "translated", @@ -28427,12 +24390,6 @@ "value" : "Ok" } }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "DE ACUERDO" - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -28461,12 +24418,6 @@ }, "Ok to MQTT" : { "localizations" : { - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Ok para MQTT" - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -28507,12 +24458,6 @@ "value" : "OLED Typ" } }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Tipo OLED" - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -28553,12 +24498,6 @@ "value" : "Nur beim Starten" } }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Sólo en el arranque" - } - }, "fr" : { "stringUnit" : { "state" : "translated", @@ -28617,12 +24556,6 @@ }, "Onboarding for licensed operators requires firmware 2.0.20 or greater. Make sure to refer to your local regulations and contact the local amateur frequency coordinators with questions." : { "localizations" : { - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "La incorporación de operadores con licencia requiere firmware 2.0.20 o superior. Asegúrese de consultar las regulaciones locales y comuníquese con los coordinadores locales de frecuencias de aficionados si tiene preguntas." - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -28663,12 +24596,6 @@ "value" : "Eine Stunde" } }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Una hora" - } - }, "fr" : { "stringUnit" : { "state" : "translated", @@ -28733,12 +24660,6 @@ "value" : "Eine Minute" } }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "un minuto" - } - }, "fr" : { "stringUnit" : { "state" : "translated", @@ -28803,12 +24724,6 @@ "value" : "Eine Sekunde" } }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "un segundo" - } - }, "fr" : { "stringUnit" : { "state" : "translated", @@ -28873,12 +24788,6 @@ "value" : "Online" } }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "En línea" - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -28907,12 +24816,6 @@ }, "Only permitted for SENSOR, TRACKER and TAK_TRACKER roles, this will inhibit all rebroadcasts, not unlike CLIENT_MUTE role." : { "localizations" : { - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Solo permitido para los roles SENSOR, TRACKER y TAK_TRACKER, esto inhibirá todas las retransmisiones, al igual que el rol CLIENT_MUTE." - } - }, "ja" : { "stringUnit" : { "state" : "translated", @@ -28935,12 +24838,6 @@ }, "Only rebroadcasts packets from the core portnums: NodeInfo, Text, Position, Telemetry, and Routing." : { "localizations" : { - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Solo retransmite paquetes desde los portnums principales: NodeInfo, Texto, Posición, Telemetría y Enrutamiento." - } - }, "ja" : { "stringUnit" : { "state" : "translated", @@ -28972,12 +24869,6 @@ "value" : "Einstellungen öffnen" } }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Abrir configuración" - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -29012,12 +24903,6 @@ }, "Optimized for 2 color displays" : { "localizations" : { - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Optimizado para pantallas de 2 colores" - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -29052,12 +24937,6 @@ "value" : "Optimiert für ATAK-Systemkommunikation, verringert die Anzahl der Routineübertragungen." } }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Optimizado para la comunicación del sistema ATAK, reduce las transmisiones de rutina." - } - }, "fr" : { "stringUnit" : { "state" : "translated", @@ -29116,12 +24995,6 @@ }, "Optional fields to include when assembling position messages. the more fields are included, the larger the message will be - leading to longer airtime and a higher risk of packet loss" : { "localizations" : { - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Campos opcionales para incluir al ensamblar mensajes de posición. Cuantos más campos se incluyan, más grande será el mensaje, lo que llevará a un mayor tiempo de emisión y a un mayor riesgo de pérdida de paquetes." - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -29156,12 +25029,6 @@ }, "Optional GPIO" : { "localizations" : { - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "GPIO opcional" - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -29196,12 +25063,6 @@ "value" : "Optionen" } }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Opciones" - } - }, "fr" : { "stringUnit" : { "state" : "translated", @@ -29260,12 +25121,6 @@ }, "OS Log Entry Details" : { "localizations" : { - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Detalles de entrada de registro del sistema operativo" - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -29294,12 +25149,6 @@ }, "OTA Updates are not supported on this NRF Device." : { "localizations" : { - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Las actualizaciones OTA no son compatibles con este dispositivo NRF." - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -29334,12 +25183,6 @@ }, "OTA Updates are not supported on your platform." : { "localizations" : { - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Las actualizaciones OTA no son compatibles con su plataforma." - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -29374,12 +25217,6 @@ }, "Other data sources" : { "localizations" : { - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Otras fuentes de datos" - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -29414,12 +25251,6 @@ "value" : "Ausgabe von Echtzeit-Fehlersuchprotokollen über die serielle Schnittstelle, Anzeige und Export von positionskorrigierten Geräteprotokollen über Bluetooth." } }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Genere registros de depuración en vivo a través de serie, vea y exporte registros de dispositivos redactados en posición a través de Bluetooth." - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -29448,12 +25279,6 @@ }, "Output pin buzzer GPIO " : { "localizations" : { - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Zumbador de pin de salida GPIO" - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -29482,12 +25307,6 @@ }, "Output pin GPIO" : { "localizations" : { - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Pin de salida GPIO" - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -29516,12 +25335,6 @@ }, "Output pin vibra GPIO" : { "localizations" : { - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Pin de salida vibración GPIO" - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -29550,12 +25363,6 @@ }, "Overlanding" : { "localizations" : { - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Por tierra" - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -29590,12 +25397,6 @@ }, "Override automatic OLED screen detection." : { "localizations" : { - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Anule la detección automática de pantalla OLED." - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -29624,12 +25425,6 @@ }, "Override default screen layout." : { "localizations" : { - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Anular el diseño de pantalla predeterminado." - } - }, "sr" : { "stringUnit" : { "state" : "translated", @@ -29640,12 +25435,6 @@ }, "Packet Count" : { "localizations" : { - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Recuento de paquetes" - } - }, "sr" : { "stringUnit" : { "state" : "translated", @@ -29662,12 +25451,6 @@ "value" : "Pairing Modus" } }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Modo de emparejamiento" - } - }, "fr" : { "stringUnit" : { "state" : "translated", @@ -29732,12 +25515,6 @@ "value" : "Passwort" } }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Contraseña" - } - }, "fr" : { "stringUnit" : { "state" : "translated", @@ -29802,12 +25579,6 @@ "value" : "Pause" } }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Pausa" - } - }, "fr" : { "stringUnit" : { "state" : "translated", @@ -29866,12 +25637,6 @@ }, "PAX Counter" : { "localizations" : { - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Contador de pasajeros" - } - }, "he" : { "stringUnit" : { "state" : "translated", @@ -29924,12 +25689,6 @@ }, "PAX Counter Config" : { "localizations" : { - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Configuración del contador PAX" - } - }, "he" : { "stringUnit" : { "state" : "translated", @@ -29976,12 +25735,6 @@ }, "PAX Counter config received: %@" : { "localizations" : { - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Configuración del contador PAX recibida: %@" - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -30016,12 +25769,6 @@ }, "PAX Counter Log" : { "localizations" : { - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Registro de contador de PAX" - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -30062,12 +25809,6 @@ "value" : "PAX Counter message received for: %@" } }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Mensaje del contador de PAX recibido de: %@" - } - }, "he" : { "stringUnit" : { "state" : "translated", @@ -30120,12 +25861,6 @@ }, "paxcounter.log" : { "localizations" : { - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "paxcounter.log" - } - }, "ja" : { "stringUnit" : { "state" : "translated", @@ -30154,12 +25889,6 @@ "value" : "Verbundenen Knoten auf Werkseinstellungen zurücksetzen" } }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Realice un restablecimiento de fábrica en el nodo al que está conectado" - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -30188,12 +25917,6 @@ }, "Philippines 433MHz" : { "localizations" : { - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Filipinas 433MHz" - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -30222,12 +25945,6 @@ }, "Philippines 868MHz" : { "localizations" : { - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Filipinas 868MHz" - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -30256,12 +25973,6 @@ }, "Philippines 915MHz" : { "localizations" : { - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Filipinas 915MHz" - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -30296,12 +26007,6 @@ "value" : "Telefon GPS" } }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "GPS del teléfono" - } - }, "fr" : { "stringUnit" : { "state" : "translated", @@ -30366,12 +26071,6 @@ "value" : "Standorteinstellungen" } }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Ubicación del teléfono" - } - }, "sr" : { "stringUnit" : { "state" : "translated", @@ -30382,12 +26081,6 @@ }, "Pin %lld" : { "localizations" : { - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Fijar %lld" - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -30416,12 +26109,6 @@ }, "Pin A" : { "localizations" : { - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Pin A" - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -30450,12 +26137,6 @@ }, "Pin B" : { "localizations" : { - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Pin B" - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -30490,12 +26171,6 @@ "value" : "PKI-basierte Knotenadministration, benötigt Firmware Version 2.5+" } }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Administración de nodos basada en PKI, requiere versión de firmware 2.5+" - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -30530,12 +26205,6 @@ }, "Please be advised that because the map report is not encrypted, your data may be stored and displayed permanently by third parties. Meshtastic does not assume responsibility for any such storage, display or disclosure of this data." : { "localizations" : { - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Tenga en cuenta que debido a que el informe del mapa no está cifrado, terceros pueden almacenar y mostrar sus datos de forma permanente. Meshtastic no asume responsabilidad por dicho almacenamiento, exhibición o divulgación de estos datos." - } - }, "ja" : { "stringUnit" : { "state" : "translated", @@ -30564,12 +26233,6 @@ "value" : "Bitte verbinde dich mit einem Funkgerät, um die Einstellungen zu ändern." } }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Conéctese a una radio para configurar los ajustes." - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -30610,12 +26273,6 @@ "value" : "Bitte lege eine Region fest" } }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Por favor establece una región" - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -30644,12 +26301,6 @@ }, "Points of Interest" : { "localizations" : { - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Puntos de interés" - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -30684,12 +26335,6 @@ "value" : "Kacke" } }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Caca" - } - }, "fr" : { "stringUnit" : { "state" : "translated", @@ -30748,12 +26393,6 @@ }, "Position" : { "localizations" : { - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Posición" - } - }, "he" : { "stringUnit" : { "state" : "translated", @@ -30806,12 +26445,6 @@ "value" : "Positionseinstellungen" } }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Configuración de posición" - } - }, "fr" : { "stringUnit" : { "state" : "translated", @@ -30876,12 +26509,6 @@ "value" : "Positionskonfiguration empfangen: %@" } }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Configuración de posición recibida: %@" - } - }, "fr" : { "stringUnit" : { "state" : "translated", @@ -30934,12 +26561,6 @@ }, "Position Exchange Failed" : { "localizations" : { - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Error en el intercambio de posición" - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -30968,12 +26589,6 @@ }, "Position Exchange Requested" : { "localizations" : { - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Intercambio de posición solicitado" - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -31002,12 +26617,6 @@ }, "Position Flags" : { "localizations" : { - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Banderas de posición" - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -31036,12 +26645,6 @@ }, "Position Log" : { "localizations" : { - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Registro de posición" - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -31076,12 +26679,6 @@ }, "Position Log %lld Points" : { "localizations" : { - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Registro de posición %lld puntos" - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -31110,12 +26707,6 @@ }, "Position Packet" : { "localizations" : { - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Paquete de posición" - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -31150,12 +26741,6 @@ "value" : "Position gesendet" } }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Posición enviada" - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -31184,12 +26769,6 @@ }, "Positions Enabled" : { "localizations" : { - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Posiciones Habilitadas" - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -31224,12 +26803,6 @@ }, "Positions will be provided by your device GPS, if you select disabled or not present you can set a fixed position." : { "localizations" : { - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Las posiciones serán proporcionadas por el GPS de su dispositivo; si selecciona deshabilitado o no presente, puede establecer una posición fija." - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -31270,12 +26843,6 @@ "value" : "Strom" } }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Fuerza" - } - }, "he" : { "stringUnit" : { "state" : "translated", @@ -31334,12 +26901,6 @@ "value" : "Stromkonfiguration" } }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Configuración de energía" - } - }, "he" : { "stringUnit" : { "state" : "translated", @@ -31392,12 +26953,6 @@ }, "Power config received: %@" : { "localizations" : { - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Configuración de energía recibida: %@" - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -31432,12 +26987,6 @@ }, "Power Metrics" : { "localizations" : { - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Métricas de energía" - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -31466,12 +27015,6 @@ }, "Power Metrics Log" : { "localizations" : { - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Registro de métricas de energía" - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -31506,12 +27049,6 @@ }, "Power Off" : { "localizations" : { - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Apagar" - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -31541,12 +27078,6 @@ "Power Options" : { "extractionState" : "stale", "localizations" : { - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Opciones de energía" - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -31581,12 +27112,6 @@ "value" : "Stromsparen" } }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Ahorro de energía" - } - }, "he" : { "stringUnit" : { "state" : "translated", @@ -31639,12 +27164,6 @@ }, "Power Screen" : { "localizations" : { - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Pantalla de energía" - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -31672,14 +27191,7 @@ } }, "Power Sensor Options" : { - "localizations" : { - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Opciones de sensores de potencia" - } - } - } + }, "Powered" : { "localizations" : { @@ -31689,12 +27201,6 @@ "value" : "Angeschaltet" } }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Motorizado" - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -31729,12 +27235,6 @@ "value" : "Genaue Position" } }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Ubicación precisa" - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -31769,12 +27269,6 @@ "value" : "Voreinstellungen" } }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Preajustes" - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -31809,12 +27303,6 @@ }, "Press Pin" : { "localizations" : { - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Pin de prensa" - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -31843,12 +27331,6 @@ }, "Pressure" : { "localizations" : { - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Presión" - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -31883,12 +27365,6 @@ "value" : "Primär" } }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Primario" - } - }, "fr" : { "stringUnit" : { "state" : "translated", @@ -31953,12 +27429,6 @@ "value" : "Erster Admin-Schlüssel" } }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Clave de administrador principal" - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -31993,12 +27463,6 @@ }, "Primary GPIO" : { "localizations" : { - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "GPIO primario" - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -32033,12 +27497,6 @@ "value" : "Privater Schlüssel" } }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Clave privada" - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -32079,12 +27537,6 @@ "value" : "Prozess" } }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Proceso" - } - }, "fr" : { "stringUnit" : { "state" : "translated", @@ -32149,12 +27601,6 @@ "value" : "Datei wird verarbeitet…" } }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Procesando archivo..." - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -32195,12 +27641,6 @@ "value" : "Projektinformationen" } }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Información del proyecto" - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -32241,12 +27681,6 @@ "value" : "Protobufs" } }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Protobufs" - } - }, "fr" : { "stringUnit" : { "state" : "translated", @@ -32311,12 +27745,6 @@ "value" : "Teile anonyme Nutzungsstatistiken und Absturzberichte." } }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Proporcione estadísticas de uso anónimas e informes de fallos." - } - }, "sr" : { "stringUnit" : { "state" : "translated", @@ -32327,12 +27755,6 @@ }, "Provide Confirmation" : { "localizations" : { - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Proporcionar confirmación" - } - }, "sr" : { "stringUnit" : { "state" : "translated", @@ -32349,12 +27771,6 @@ "value" : "Öffentlicher Schlüssel" } }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Clave pública" - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -32389,12 +27805,6 @@ }, "Public Key Encryption" : { "localizations" : { - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Cifrado de clave pública" - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -32429,12 +27839,6 @@ }, "Public Key Mismatch" : { "localizations" : { - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "La clave pública no coincide" - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -32469,12 +27873,6 @@ }, "PWD" : { "localizations" : { - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "PCD" - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -32509,12 +27907,6 @@ "value" : "Fragezeichen" } }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Pregunta" - } - }, "fr" : { "stringUnit" : { "state" : "translated", @@ -32573,12 +27965,6 @@ }, "Radiation" : { "localizations" : { - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Radiación" - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -32613,12 +27999,6 @@ "value" : "Geräteeinstellungen" } }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Configuración de radio" - } - }, "fr" : { "stringUnit" : { "state" : "translated", @@ -32683,12 +28063,6 @@ "value" : "RAK Drehimpulsgeber Modul" } }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Codificador rotatorio RAK" - } - }, "fr" : { "stringUnit" : { "state" : "translated", @@ -32753,12 +28127,6 @@ "value" : "Entfernungstest" } }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Prueba de rango" - } - }, "fr" : { "stringUnit" : { "state" : "translated", @@ -32823,12 +28191,6 @@ "value" : "Entfernungstest Konfiguration" } }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Configuración de prueba de rango" - } - }, "fr" : { "stringUnit" : { "state" : "translated", @@ -32893,12 +28255,6 @@ "value" : "Range Test Modul konfiguration empfangen: %@" } }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Configuración del módulo de prueba de rango recibida: %@" - } - }, "fr" : { "stringUnit" : { "state" : "translated", @@ -32963,12 +28319,6 @@ "value" : "Neustart" } }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Reiniciar" - } - }, "fr" : { "stringUnit" : { "state" : "translated", @@ -33033,12 +28383,6 @@ "value" : "Knoten neustarten?" } }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "¿Reiniciar el nodo?" - } - }, "fr" : { "stringUnit" : { "state" : "translated", @@ -33097,12 +28441,6 @@ }, "Rebroadcast any observed message, if it was on our private channel or from another mesh with the same lora params." : { "localizations" : { - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Retransmitir cualquier mensaje observado, si fue en nuestro canal privado o desde otra malla con los mismos parámetros de lora." - } - }, "ja" : { "stringUnit" : { "state" : "translated", @@ -33125,12 +28463,6 @@ }, "Rebroadcast Mode" : { "localizations" : { - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Modo de retransmisión" - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -33165,12 +28497,6 @@ }, "Receive data (rxd) GPIO pin" : { "localizations" : { - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Recibir datos (rxd) pin GPIO" - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -33205,12 +28531,6 @@ "value" : "Negative Empfangsbestätigung empfangen" } }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Recibí un reconocimiento negativo." - } - }, "fr" : { "stringUnit" : { "state" : "translated", @@ -33276,12 +28596,6 @@ "value" : "Empfangsbestätigung" } }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Confirmación recibida" - } - }, "fr" : { "stringUnit" : { "state" : "translated", @@ -33350,12 +28664,6 @@ "value" : "Recipient Ack" } }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Confirmación del destinatario" - } - }, "fr" : { "stringUnit" : { "state" : "translated", @@ -33423,12 +28731,6 @@ "value" : "Route aufzeichnen" } }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Ruta de grabación" - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -33457,12 +28759,6 @@ }, "Refresh device metadata" : { "localizations" : { - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Actualizar metadatos del dispositivo" - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -33491,12 +28787,6 @@ }, "Regenerate Private Key" : { "localizations" : { - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Regenerar clave privada" - } - }, "ja" : { "stringUnit" : { "state" : "translated", @@ -33519,12 +28809,6 @@ "value" : "Region" } }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Región" - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -33565,12 +28849,6 @@ "value" : "Regionale Einschaltdauergrenze erreicht" } }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Se alcanzó el límite del ciclo de trabajo regional" - } - }, "fr" : { "stringUnit" : { "state" : "translated", @@ -33639,12 +28917,6 @@ }, "Release Notes" : { "localizations" : { - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Notas de la versión" - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -33673,12 +28945,6 @@ }, "Remote administration for: %@" : { "localizations" : { - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Administración remota para: %@" - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -33707,12 +28973,6 @@ }, "Remote Legacy Admin: %@" : { "localizations" : { - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Administrador remoto heredado: %@" - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -33741,12 +29001,6 @@ }, "Remote PKI Admin: %@" : { "localizations" : { - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Administrador de PKI remoto: %@" - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -33781,12 +29035,6 @@ "value" : "Entfernen" } }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Eliminar" - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -33821,12 +29069,6 @@ "value" : "Von Favoriten entfernen" } }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Quitar de favoritos" - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -33855,12 +29097,6 @@ }, "Remove from ignored" : { "localizations" : { - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Quitar de ignorado" - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -33901,12 +29137,6 @@ "value" : "Repeater" } }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Reloj de repetición" - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -33941,12 +29171,6 @@ }, "Replace Channels" : { "localizations" : { - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Reemplazar canales" - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -33981,12 +29205,6 @@ "value" : "Antworten" } }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Responder" - } - }, "fr" : { "stringUnit" : { "state" : "translated", @@ -34045,12 +29263,6 @@ }, "Request Legacy Admin: %@" : { "localizations" : { - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Solicitar administrador heredado: %@" - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -34079,12 +29291,6 @@ }, "Request PKI Admin: %@" : { "localizations" : { - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Solicitar administrador de PKI: %@" - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -34113,12 +29319,6 @@ }, "Requested Canned Messages Module Messages for node: %@" : { "localizations" : { - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Mensajes del módulo de mensajes predefinidos solicitados para el nodo: %@" - } - }, "fr" : { "stringUnit" : { "state" : "translated", @@ -34177,12 +29377,6 @@ }, "Requires that there be an accelerometer on your device." : { "localizations" : { - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Requiere que haya un acelerómetro en su dispositivo." - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -34217,12 +29411,6 @@ "value" : "App-Einstellungen zurücksetzen" } }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Restablecer la configuración de la aplicación" - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -34263,12 +29451,6 @@ "value" : "Knotendatenbank zurücksetzen" } }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Restablecer NodeDB" - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -34303,12 +29485,6 @@ "value" : "Neustarten" } }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Reanudar" - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -34343,12 +29519,6 @@ "value" : "Verbundenen Knoten neustarten" } }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Reinicie en el nodo al que está conectado" - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -34377,12 +29547,6 @@ }, "Restore" : { "localizations" : { - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Restaurar" - } - }, "ja" : { "stringUnit" : { "state" : "translated", @@ -34405,12 +29569,6 @@ "value" : "Fortsetzen" } }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Reanudar" - } - }, "fr" : { "stringUnit" : { "state" : "translated", @@ -34469,12 +29627,6 @@ }, "Retreiving nodes . ." : { "localizations" : { - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Recuperando nodos. ." - } - }, "sr" : { "stringUnit" : { "state" : "translated", @@ -34485,12 +29637,6 @@ }, "Retreiving nodes %lld" : { "localizations" : { - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Recuperando nodos %lld" - } - }, "sr" : { "stringUnit" : { "state" : "translated", @@ -34501,12 +29647,6 @@ }, "Retrieving nodes" : { "localizations" : { - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Recuperando nodos" - } - }, "sr" : { "stringUnit" : { "state" : "translated", @@ -34517,12 +29657,6 @@ }, "Retrieving nodes %lld" : { "localizations" : { - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Recuperando nodos %lld" - } - }, "sr" : { "stringUnit" : { "state" : "translated", @@ -34533,12 +29667,6 @@ }, "Retrying (attempt %lld)" : { "localizations" : { - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Reintentando (intento %lld)" - } - }, "sr" : { "stringUnit" : { "state" : "translated", @@ -34555,12 +29683,6 @@ "value" : "App bewerten" } }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Revisa la aplicación" - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -34601,12 +29723,6 @@ "value" : "Rechts" } }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Bien" - } - }, "fr" : { "stringUnit" : { "state" : "translated", @@ -34671,12 +29787,6 @@ "value" : "Klingelton" } }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Tono de llamada" - } - }, "fr" : { "stringUnit" : { "state" : "translated", @@ -34741,12 +29851,6 @@ "value" : "Klingelton Konfiguration" } }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Configuración de tono de llamada" - } - }, "he" : { "stringUnit" : { "state" : "translated", @@ -34799,12 +29903,6 @@ }, "Ringtone Transfer Language" : { "localizations" : { - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Idioma de transferencia de tono de llamada" - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -34845,12 +29943,6 @@ }, "Ringtone Transfer Language(RTTTL) Ringtone String used by supported buzzers in external notifications." : { "localizations" : { - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Lenguaje de transferencia de tono de llamada (RTTTL) Cadena de tono utilizada por los timbres compatibles en notificaciones externas." - } - }, "he" : { "stringUnit" : { "state" : "translated", @@ -34909,12 +30001,6 @@ "value" : "Rolle" } }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Role" - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -34955,12 +30041,6 @@ "value" : "Rolle: %@" } }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Role: %@" - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -34995,12 +30075,6 @@ "value" : "Rollen" } }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Roles" - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -35029,12 +30103,6 @@ }, "Root Topic" : { "localizations" : { - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Tema raíz" - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -35063,12 +30131,6 @@ }, "Rotary 1" : { "localizations" : { - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Giratorio 1" - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -35103,12 +30165,6 @@ }, "Route Back: %@" : { "localizations" : { - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Ruta de regreso: %@" - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -35137,12 +30193,6 @@ }, "Route Lines" : { "localizations" : { - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Líneas de ruta" - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -35177,12 +30227,6 @@ "value" : "Routenliste" } }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Lista de rutas" - } - }, "sr" : { "stringUnit" : { "state" : "translated", @@ -35199,12 +30243,6 @@ "value" : "Route aufzeichnen" } }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Grabador de ruta" - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -35245,12 +30283,6 @@ "value" : "Routenaufzeichnung pausiert" } }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Grabación de ruta en pausa" - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -35285,12 +30317,6 @@ "value" : "Route: %@" } }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Ruta: %@" - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -35325,12 +30351,6 @@ "value" : "Router" } }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Enrutador" - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -35371,12 +30391,6 @@ "value" : "Router mit Verzögerung" } }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Enrutador tarde" - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -35411,12 +30425,6 @@ "value" : "Routenliste" } }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Rutas" - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -35457,12 +30465,6 @@ "value" : "Routing empfangen für RequestID: %@ Ack Status: %@" } }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Enrutamiento recibido para RequestID: %@ Estado de confirmación: %@" - } - }, "fr" : { "stringUnit" : { "state" : "translated", @@ -35521,12 +30523,6 @@ }, "RSSI %@ dBm" : { "localizations" : { - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "RSSI %@dBm" - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -35555,12 +30551,6 @@ }, "RSSI %ddB" : { "localizations" : { - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "RSSI %ddB" - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -35589,12 +30579,6 @@ }, "RSSI %llddB" : { "localizations" : { - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "RSSI %llddB" - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -35629,12 +30613,6 @@ "value" : "RTTTL Klingeltonkonfiguration empfangen: %@" } }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Configuración de tono de llamada RTTTL recibida: %@" - } - }, "fr" : { "stringUnit" : { "state" : "translated", @@ -35693,12 +30671,6 @@ }, "Russia" : { "localizations" : { - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Rusia" - } - }, "ja" : { "stringUnit" : { "state" : "translated", @@ -35721,12 +30693,6 @@ }, "RX Boosted Gain" : { "localizations" : { - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Ganancia impulsada por RX" - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -35755,12 +30721,6 @@ }, "Same as behavior as ALL but skips packet decoding and simply rebroadcasts them. Only available in Repeater role. Setting this on any other roles will result in ALL behavior." : { "localizations" : { - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Igual que el comportamiento de ALL, pero omite la decodificación de paquetes y simplemente los retransmite. Sólo disponible en rol de Repetidor. Establecer esto en cualquier otro rol dará como resultado TODOS los comportamientos." - } - }, "ja" : { "stringUnit" : { "state" : "translated", @@ -35789,12 +30749,6 @@ "value" : "Satellit" } }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Satélite" - } - }, "he" : { "stringUnit" : { "state" : "translated", @@ -35841,12 +30795,6 @@ }, "Satellite Flyover" : { "localizations" : { - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Sobrevuelo satelital" - } - }, "fr" : { "stringUnit" : { "state" : "translated", @@ -35911,12 +30859,6 @@ "value" : "Satelliten" } }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "sábados" - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -35951,12 +30893,6 @@ "value" : "Satelliten Schätzung %lld" } }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Estimación de sats %lld" - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -35991,12 +30927,6 @@ "value" : "Satelliten in Sicht: %@" } }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Sats a la vista: %@" - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -36031,12 +30961,6 @@ "value" : "Speichern" } }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Ahorrar" - } - }, "fr" : { "stringUnit" : { "state" : "translated", @@ -36095,12 +31019,6 @@ }, "Save Channel Settings" : { "localizations" : { - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Guardar configuración del canal" - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -36135,12 +31053,6 @@ "value" : "Speichere Konfiguration für %@" } }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Guardar configuración para %@" - } - }, "fr" : { "stringUnit" : { "state" : "translated", @@ -36205,12 +31117,6 @@ "value" : "Benutzerkonfiguration nach %@ speichern?" } }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "¿Guardar configuración de usuario en %@?" - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -36245,12 +31151,6 @@ }, "Saves a CSV with the range test message details, currently only available on ESP32 devices with a web server." : { "localizations" : { - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Guarda un CSV con los detalles del mensaje de prueba de rango, actualmente solo disponible en dispositivos ESP32 con un servidor web." - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -36285,12 +31185,6 @@ }, "Scan this QR code to add %@ to another device." : { "localizations" : { - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Escanee este código QR para agregar %@ a otro dispositivo." - } - }, "ja" : { "stringUnit" : { "state" : "translated", @@ -36313,12 +31207,6 @@ }, "Screen on for" : { "localizations" : { - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Pantalla encendida para" - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -36353,12 +31241,6 @@ "value" : "Suchen" } }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Buscar" - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -36387,12 +31269,6 @@ }, "Second" : { "localizations" : { - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Segundo" - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -36427,12 +31303,6 @@ "value" : "Sekundär" } }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Secundario" - } - }, "fr" : { "stringUnit" : { "state" : "translated", @@ -36497,12 +31367,6 @@ "value" : "Zweiter Admin-Schlüssel" } }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Clave de administrador secundario" - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -36543,12 +31407,6 @@ "value" : "Sicherheit" } }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Seguridad" - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -36589,12 +31447,6 @@ "value" : "Sicherheitskonfiguration" } }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Configuración de seguridad" - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -36635,12 +31487,6 @@ "value" : "Sicherheitskonfigurationseinstellungen erfordern eine Firmware mit Version 2.5 oder höher" } }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Los ajustes de configuración de seguridad requieren una versión de firmware 2.5+" - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -36681,12 +31527,6 @@ "value" : "Auswählen" } }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Seleccionar" - } - }, "fr" : { "stringUnit" : { "state" : "translated", @@ -36751,12 +31591,6 @@ "value" : "Kanal wählen" } }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Seleccione un canal" - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -36785,12 +31619,6 @@ }, "Select a conversation" : { "localizations" : { - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Seleccione una conversación" - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -36819,12 +31647,6 @@ }, "Select a conversation type" : { "localizations" : { - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Seleccione un tipo de conversación" - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -36858,23 +31680,10 @@ } }, "Select a Node" : { - "localizations" : { - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Seleccione un nodo" - } - } - } + }, "Select a node from the drop down to manage connected or remote devices." : { "localizations" : { - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Seleccione un nodo del menú desplegable para administrar dispositivos conectados o remotos." - } - }, "ja" : { "stringUnit" : { "state" : "translated", @@ -36897,12 +31706,6 @@ }, "Select a Trace Route" : { "localizations" : { - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Seleccione una ruta de seguimiento" - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -36934,12 +31737,6 @@ }, "Select Channel" : { "localizations" : { - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Seleccionar canal" - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -36974,12 +31771,6 @@ "value" : "Datei auswählen" } }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Seleccionar archivo de mapa" - } - }, "sr" : { "stringUnit" : { "state" : "translated", @@ -36996,12 +31787,6 @@ "value" : "Als kritisch eingestufte Mitteilungen ignorieren den Stummschalter und die 'Nicht stören'-Einstellungen des Benachrichtigungszentrums." } }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Los paquetes seleccionados enviados como críticos ignorarán el interruptor de silencio y la configuración de No molestar en el centro de notificaciones del sistema operativo." - } - }, "sr" : { "stringUnit" : { "state" : "translated", @@ -37018,12 +31803,6 @@ "value" : "Senden" } }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Enviar" - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -37058,12 +31837,6 @@ "value" : "Sende ${messageContent} an ${channelNumber}" } }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Enviar ${messageContent} a ${channelNumber}" - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -37092,12 +31865,6 @@ }, "Send ${messageContent} to ${nodeNumber}" : { "localizations" : { - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Enviar ${messageContent} a ${nodeNumber}" - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -37126,12 +31893,6 @@ }, "Send a Direct Message" : { "localizations" : { - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Enviar un mensaje directo" - } - }, "sr" : { "stringUnit" : { "state" : "translated", @@ -37148,12 +31909,6 @@ "value" : "Gruppennachricht senden" } }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Enviar un mensaje grupal" - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -37182,12 +31937,6 @@ }, "Send a heartbeat to advertise the server's presence." : { "localizations" : { - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Envíe un latido para anunciar la presencia del servidor." - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -37216,12 +31965,6 @@ }, "Send a message to a certain meshtastic channel" : { "localizations" : { - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Enviar un mensaje a un determinado canal meshtastic" - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -37250,12 +31993,6 @@ }, "Send a message to a certain meshtastic node" : { "localizations" : { - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Enviar un mensaje a un determinado nodo meshtastic" - } - }, "sr" : { "stringUnit" : { "state" : "translated", @@ -37266,12 +32003,6 @@ }, "Send a position on the primary channel when the user button is triple clicked." : { "localizations" : { - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Envíe una posición en el canal principal cuando se haga triple clic en el botón del usuario." - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -37312,12 +32043,6 @@ "value" : "Herunterfahren an verbundenen Knoten senden" } }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Envía un apagado al nodo al que estás conectado" - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -37352,12 +32077,6 @@ "value" : "Wegpunkt senden" } }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Enviar un punto de ruta" - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -37386,12 +32105,6 @@ }, "Send ASCII bell with alert message. Useful for triggering external notification on bell." : { "localizations" : { - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Enviar campana ASCII con mensaje de alerta. Útil para activar notificaciones externas al tocar el timbre." - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -37432,12 +32145,6 @@ "value" : "Sende Glocke" } }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Enviar campana" - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -37478,12 +32185,6 @@ "value" : "Herzschlag senden" } }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Enviar latido" - } - }, "fr" : { "stringUnit" : { "state" : "translated", @@ -37548,12 +32249,6 @@ "value" : "Mitteilungen senden" } }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Enviar notificaciones" - } - }, "sr" : { "stringUnit" : { "state" : "translated", @@ -37564,12 +32259,6 @@ }, "Send Reboot OTA" : { "localizations" : { - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Enviar Reiniciar OTA" - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -37604,12 +32293,6 @@ }, "Sender Interval" : { "localizations" : { - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Intervalo del remitente" - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -37650,12 +32333,6 @@ "value" : "Sensor" } }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Sensor" - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -37690,12 +32367,6 @@ }, "Sensor options" : { "localizations" : { - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Opciones de sensores" - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -37725,12 +32396,6 @@ "Sensor Options" : { "extractionState" : "stale", "localizations" : { - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Opciones de sensores" - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -37759,12 +32424,6 @@ }, "Sent a Channel for: %@ Channel Index %d" : { "localizations" : { - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Envió un canal para: %@ Índice de canales %d" - } - }, "fr" : { "stringUnit" : { "state" : "translated", @@ -37829,12 +32488,6 @@ "value" : "LoRa.Config gesendet für: %@" } }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Envió un LoRa.Config para: %@" - } - }, "fr" : { "stringUnit" : { "state" : "translated", @@ -37900,12 +32553,6 @@ "value" : "Position von Apple Gerät an Knoten gesendet: %@" } }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Envió un paquete de posición desde el GPS del dispositivo Apple al nodo: %@@" - } - }, "fr" : { "stringUnit" : { "state" : "translated", @@ -37970,12 +32617,6 @@ "value" : "Sende Traceroute Anforderung zu Knoten: %@" } }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Envió una solicitud de ruta de seguimiento al nodo: %@" - } - }, "fr" : { "stringUnit" : { "state" : "translated", @@ -38040,12 +32681,6 @@ "value" : "Wegpunkt gesendet von: %@" } }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Enviado un paquete de waypoint desde: %@" - } - }, "fr" : { "stringUnit" : { "state" : "translated", @@ -38110,12 +32745,6 @@ "value" : "Sende Nachricht %@ von %@ an %@" } }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Mensaje enviado %@ de %@ a %@" - } - }, "fr" : { "stringUnit" : { "state" : "translated", @@ -38180,12 +32809,6 @@ "value" : "Sequenznummer" } }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Número de secuencia" - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -38220,12 +32843,6 @@ "value" : "Sequenz: %@" } }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Secuencia: %@" - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -38254,12 +32871,6 @@ }, "Serial" : { "localizations" : { - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "De serie" - } - }, "fr" : { "stringUnit" : { "state" : "translated", @@ -38324,12 +32935,6 @@ "value" : "Serial Konfiguration" } }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Configuración en serie" - } - }, "fr" : { "stringUnit" : { "state" : "translated", @@ -38394,12 +32999,6 @@ "value" : "Serielle Konsole" } }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Consola serie" - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -38434,12 +33033,6 @@ "value" : "Serielle Konsole über die Stream-API." } }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Consola serial a través de Stream API." - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -38474,12 +33067,6 @@ "value" : "Serial Modul Konfiguration empfangen: %@" } }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Configuración del módulo serie recibida: %@" - } - }, "fr" : { "stringUnit" : { "state" : "translated", @@ -38538,12 +33125,6 @@ }, "Series" : { "localizations" : { - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Serie" - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -38578,12 +33159,6 @@ "value" : "Server" } }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Servidor" - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -38618,12 +33193,6 @@ "value" : "Serveradresse" } }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Dirección del servidor" - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -38652,12 +33221,6 @@ }, "Server Option" : { "localizations" : { - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Opción de servidor" - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -38686,12 +33249,6 @@ }, "Set" : { "localizations" : { - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Colocar" - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -38726,12 +33283,6 @@ "value" : "Setze LoRa Region" } }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Establecer región LoRa" - } - }, "fr" : { "stringUnit" : { "state" : "translated", @@ -38790,12 +33341,6 @@ }, "Set the GPIO pins for RXD and TXD." : { "localizations" : { - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Configure los pines GPIO para RXD y TXD." - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -38824,12 +33369,6 @@ }, "Set to current location" : { "localizations" : { - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Establecer en la ubicación actual" - } - }, "ja" : { "stringUnit" : { "state" : "translated", @@ -38846,12 +33385,6 @@ }, "Sets the maximum number of hops, default is 3. Increasing hops also increases congestion and should be used carefully. O hop broadcast messages will not get ACKs." : { "localizations" : { - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Establece el número máximo de saltos; el valor predeterminado es 3. El aumento de saltos también aumenta la congestión y debe usarse con cuidado. Los mensajes de difusión de O hop no recibirán ACK." - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -38880,12 +33413,6 @@ }, "Sets the screen clock format to 12-hour." : { "localizations" : { - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Establece el formato del reloj de la pantalla en 12 horas." - } - }, "ja" : { "stringUnit" : { "state" : "translated", @@ -38908,12 +33435,6 @@ "value" : "Einstellungen" } }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "ajustes" - } - }, "sr" : { "stringUnit" : { "state" : "translated", @@ -38930,12 +33451,6 @@ "value" : "Einstellungen" } }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Ajustes" - } - }, "fr" : { "stringUnit" : { "state" : "translated", @@ -39000,12 +33515,6 @@ "value" : "Zweiundsiebzig Stunden" } }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Setenta y dos horas" - } - }, "fr" : { "stringUnit" : { "state" : "translated", @@ -39064,12 +33573,6 @@ }, "Share Contact QR" : { "localizations" : { - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Compartir Contacto QR" - } - }, "ja" : { "stringUnit" : { "state" : "translated", @@ -39098,12 +33601,6 @@ "value" : "Standort teilen" } }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Compartir ubicación" - } - }, "sr" : { "stringUnit" : { "state" : "translated", @@ -39120,12 +33617,6 @@ "value" : "Kanal QR Code teilen" } }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Compartir código QR" - } - }, "fr" : { "stringUnit" : { "state" : "translated", @@ -39190,12 +33681,6 @@ "value" : "QR Code & Link teilen" } }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Compartir código QR y enlace" - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -39230,12 +33715,6 @@ "value" : "Teile deinen Standort in Echtzeit und koordiniere deine Gruppe mithilfe integrierter GPS-Funktionen." } }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Comparta su ubicación en tiempo real y mantenga a su grupo coordinado con funciones de GPS integradas." - } - }, "sr" : { "stringUnit" : { "state" : "translated", @@ -39252,12 +33731,6 @@ "value" : "Gemeinsamer Schlüssel" } }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Clave compartida" - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -39292,12 +33765,6 @@ "value" : "Meshtastic Kanäle teilen" } }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Compartir canales Meshtastic" - } - }, "fr" : { "stringUnit" : { "state" : "translated", @@ -39362,12 +33829,6 @@ "value" : "Kurzname" } }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Nombre corto" - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -39402,12 +33863,6 @@ }, "Short Range - Fast" : { "localizations" : { - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Corto alcance - Rápido" - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -39436,12 +33891,6 @@ }, "Short Range - Slow" : { "localizations" : { - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Corto alcance - Lento" - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -39470,12 +33919,6 @@ }, "Short Range - Turbo" : { "localizations" : { - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Corto alcance - Turbo" - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -39504,12 +33947,6 @@ }, "Show a confirmation dialog before performing the factory reset" : { "localizations" : { - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Mostrar un cuadro de diálogo de confirmación antes de realizar el restablecimiento de fábrica" - } - }, "sr" : { "stringUnit" : { "state" : "translated", @@ -39526,12 +33963,6 @@ "value" : "Zeige Alarme" } }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Mostrar alertas" - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -39566,12 +33997,6 @@ "value" : "Zeige Alarme" } }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Mostrar alertas" - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -39606,12 +34031,6 @@ "value" : "Zeige Knoten" } }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Mostrar nodos" - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -39646,12 +34065,6 @@ "value" : "Zeige auf dem Gerätebildschirm" } }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Mostrar en la pantalla del dispositivo" - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -39686,12 +34099,6 @@ "value" : "Zeige auf der Netzwerkkarte." } }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Mostrar en el mapa de malla." - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -39727,12 +34134,6 @@ "value" : "Zeige Wegpunkte" } }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Mostrar puntos de ruta" - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -39761,12 +34162,6 @@ }, "Shows information for the connected Lora radio. You can swipe left to disconnect the radio and long press to start the live activity." : { "localizations" : { - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Muestra información de la radio Lora conectada. Puede deslizar hacia la izquierda para desconectar la radio y mantener presionada para iniciar la actividad en vivo." - } - }, "sr" : { "stringUnit" : { "state" : "translated", @@ -39783,12 +34178,6 @@ "value" : "Herunterfahren" } }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Cerrar" - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -39823,12 +34212,6 @@ "value" : "Knoten herunterfahren?" } }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "¿Cerrar el nodo?" - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -39863,12 +34246,6 @@ "value" : "Knoten herunterfahren?" } }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "¿Apagar el nodo?" - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -39903,12 +34280,6 @@ "value" : "Herunterfahren bei Stromunterbruch" } }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Apagado por pérdida de energía" - } - }, "he" : { "stringUnit" : { "state" : "translated", @@ -39961,12 +34332,6 @@ }, "Signal %@" : { "localizations" : { - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Señal %@" - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -40001,12 +34366,6 @@ "value" : "Einfach" } }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Simple" - } - }, "fr" : { "stringUnit" : { "state" : "translated", @@ -40065,12 +34424,6 @@ }, "Singapore 923MHz" : { "localizations" : { - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Singapur 923MHz" - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -40105,12 +34458,6 @@ "value" : "Sechs Stunden" } }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Seis horas" - } - }, "fr" : { "stringUnit" : { "state" : "translated", @@ -40175,12 +34522,6 @@ "value" : "Skifahren" } }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Esquiar" - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -40215,12 +34556,6 @@ }, "Smart Position" : { "localizations" : { - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Posición inteligente" - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -40249,12 +34584,6 @@ }, "SNR" : { "localizations" : { - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "SNR" - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -40283,12 +34612,6 @@ }, "SNR %@ dB" : { "localizations" : { - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "SNR %@dB" - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -40317,12 +34640,6 @@ }, "SNR %@dB" : { "localizations" : { - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "SNR %@dB" - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -40351,12 +34668,6 @@ }, "Soil Moisture" : { "localizations" : { - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Humedad del suelo" - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -40385,12 +34696,6 @@ }, "Soil Temp" : { "localizations" : { - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Temperatura del suelo" - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -40420,12 +34725,6 @@ "Specifies how long the monitored GPIO should output." : { "extractionState" : "stale", "localizations" : { - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Especifica cuánto tiempo debe emitir el GPIO monitoreado." - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -40460,12 +34759,6 @@ "value" : "Geschwindigkeit" } }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Velocidad" - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -40500,12 +34793,6 @@ "value" : "Geschwindigkeit %@" } }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Velocidad %@" - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -40540,12 +34827,6 @@ "value" : "Geschwindigkeit: %@" } }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Velocidad: %@" - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -40580,12 +34861,6 @@ "value" : "App-Entwicklung unterstützen" } }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Desarrollo de aplicaciones para patrocinadores" - } - }, "ja" : { "stringUnit" : { "state" : "translated", @@ -40602,12 +34877,6 @@ }, "Spread Factor" : { "localizations" : { - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Factor de dispersión" - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -40642,12 +34911,6 @@ "value" : "SSID" } }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "SSID" - } - }, "fr" : { "stringUnit" : { "state" : "translated", @@ -40706,12 +34969,6 @@ }, "Standard" : { "localizations" : { - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Estándar" - } - }, "he" : { "stringUnit" : { "state" : "translated", @@ -40758,12 +35015,6 @@ }, "Standard Muted" : { "localizations" : { - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Estándar silenciado" - } - }, "fr" : { "stringUnit" : { "state" : "translated", @@ -40828,12 +35079,6 @@ "value" : "Start" } }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Comenzar" - } - }, "fr" : { "stringUnit" : { "state" : "translated", @@ -40892,12 +35137,6 @@ }, "State Broadcast Interval" : { "localizations" : { - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Intervalo de transmisión estatal" - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -40932,12 +35171,6 @@ "value" : "Überall in Verbindung bleiben" } }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Manténgase conectado en cualquier lugar" - } - }, "sr" : { "stringUnit" : { "state" : "translated", @@ -40948,12 +35181,6 @@ }, "Store & Forward" : { "localizations" : { - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Almacenar y reenviar" - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -40988,12 +35215,6 @@ }, "Store & Forward Config" : { "localizations" : { - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Almacenar y reenviar configuración" - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -41028,12 +35249,6 @@ }, "Store & Forward module config received: %@" : { "localizations" : { - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Configuración del módulo Store & Forward recibida: %@" - } - }, "fr" : { "stringUnit" : { "state" : "translated", @@ -41092,12 +35307,6 @@ }, "Store and forward servers require an ESP32 device with PSRAM or Linux Native." : { "localizations" : { - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Los servidores de almacenamiento y reenvío requieren un dispositivo ESP32 con PSRAM o Linux Native." - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -41126,12 +35335,6 @@ }, "Subscribed" : { "localizations" : { - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "suscrito" - } - }, "he" : { "stringUnit" : { "state" : "translated", @@ -41166,12 +35369,6 @@ }, "Subsystem" : { "localizations" : { - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "subsistema" - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -41212,12 +35409,6 @@ "value" : "Successfully uploaded '%1$@' with %2$lld overlays" } }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "'%@' subido correctamente con %lld superposiciones" - } - }, "sr" : { "stringUnit" : { "state" : "translated", @@ -41234,12 +35425,6 @@ "value" : "Unterstützt" } }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Apoyado" - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -41268,12 +35453,6 @@ }, "Supported I2C Connected sensors will be detected automatically, sensors are BMP280, BME280, BME680, MCP9808, INA219, INA260, LPS22 and SHTC3." : { "localizations" : { - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Los sensores conectados I2C compatibles se detectarán automáticamente, los sensores son BMP280, BME280, BME680, MCP9808, INA219, INA260, LPS22 y SHTC3." - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -41308,12 +35487,6 @@ }, "Table" : { "localizations" : { - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Mesa" - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -41342,12 +35515,6 @@ }, "Taiwan" : { "localizations" : { - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Taiwán" - } - }, "ja" : { "stringUnit" : { "state" : "translated", @@ -41376,12 +35543,6 @@ "value" : "TAK" } }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "NO" - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -41422,12 +35583,6 @@ "value" : "TAK Tracker" } }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "SÍ rastreador" - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -41462,12 +35617,6 @@ }, "Takes a Meshtastic channel URL and saves the channel settings." : { "localizations" : { - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Toma la URL de un canal Meshtastic y guarda la configuración del canal." - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -41496,12 +35645,6 @@ }, "Takes a Meshtastic contact URL and saves it to the nodes database" : { "localizations" : { - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Toma una URL de contacto Meshtastic y la guarda en la base de datos de nodos" - } - }, "ja" : { "stringUnit" : { "state" : "translated", @@ -41530,12 +35673,6 @@ "value" : "Tapback Antwort" } }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Tapback" - } - }, "fr" : { "stringUnit" : { "state" : "translated", @@ -41593,14 +35730,6 @@ } }, "TCP" : { - "localizations" : { - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "tcp" - } - } - }, "shouldTranslate" : false }, "Telemetry" : { @@ -41611,12 +35740,6 @@ "value" : "Telemetrie (Sensoren)" } }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Telemetria" - } - }, "fr" : { "stringUnit" : { "state" : "translated", @@ -41681,12 +35804,6 @@ "value" : "Telemetrie Einstellungen" } }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Configuración de telemetría" - } - }, "fr" : { "stringUnit" : { "state" : "translated", @@ -41751,12 +35868,6 @@ "value" : "Telemetrie Modul Konfiguration empfangen: %@" } }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Configuración del módulo de telemetría recibida: %@" - } - }, "fr" : { "stringUnit" : { "state" : "translated", @@ -41821,12 +35932,6 @@ "value" : "Temp" } }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Temperatura" - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -41861,12 +35966,6 @@ "value" : "Temperatur" } }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Temperatura" - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -41901,12 +36000,6 @@ "value" : "Zehn Minuten" } }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "diez minutos" - } - }, "fr" : { "stringUnit" : { "state" : "translated", @@ -41971,12 +36064,6 @@ "value" : "Zehn Sekunden" } }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Diez segundos" - } - }, "fr" : { "stringUnit" : { "state" : "translated", @@ -42041,12 +36128,6 @@ "value" : "Dritter Admin-Schlüssel" } }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Clave de administrador terciario" - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -42087,12 +36168,6 @@ "value" : "Textnachricht" } }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Mensaje de texto" - } - }, "fr" : { "stringUnit" : { "state" : "translated", @@ -42151,12 +36226,6 @@ }, "TFT Full Color Displays" : { "localizations" : { - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Pantallas TFT a todo color" - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -42185,12 +36254,6 @@ }, "Thailand" : { "localizations" : { - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Tailandia" - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -42219,12 +36282,6 @@ }, "The amount of time to wait before we consider your packet as done." : { "localizations" : { - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "La cantidad de tiempo que debemos esperar antes de que consideremos que su paquete está listo." - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -42253,12 +36310,6 @@ }, "The compass heading on the screen outside of the circle will always point north." : { "localizations" : { - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "El rumbo de la brújula en la pantalla fuera del círculo siempre apuntará al norte." - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -42293,12 +36344,6 @@ "value" : "Der Taupunkt ist gerade %@" } }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "El punto de rocío es %@ en este momento." - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -42327,12 +36372,6 @@ }, "The fastest that position updates will be sent if the minimum distance has been satisfied" : { "localizations" : { - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Lo más rápido que se enviarán las actualizaciones de posición si se ha cumplido la distancia mínima" - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -42367,12 +36406,6 @@ "value" : "Die letzten 4 Zeichen der MAC-Adresse des Geräts werden an den Kurznamen angehängt, um den BLE-Namen des Geräts festzulegen. Der Kurzname kann bis zu 4 Byte lang sein." } }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Los últimos 4 de la dirección MAC del dispositivo se agregarán al nombre corto para configurar el nombre BLE del dispositivo. El nombre corto puede tener hasta 4 bytes de longitud." - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -42407,12 +36440,6 @@ }, "The maximum interval that can elapse without a node broadcasting a position" : { "localizations" : { - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "El intervalo máximo que puede transcurrir sin que un nodo transmita una posición." - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -42441,12 +36468,6 @@ }, "The Meshtastic Apple apps support firmware version %@ and above." : { "localizations" : { - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Las aplicaciones Meshtastic de Apple admiten la versión de firmware %@ y superior." - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -42481,12 +36502,6 @@ }, "The minimum distance change in meters to be considered for a smart position broadcast." : { "localizations" : { - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "El cambio mínimo de distancia en metros a considerar para una transmisión de posición inteligente." - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -42527,12 +36542,6 @@ "value" : "Das Paket ist zu groß" } }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "El paquete es demasiado grande." - } - }, "fr" : { "stringUnit" : { "state" : "translated", @@ -42597,12 +36606,6 @@ "value" : "Der erste öffentliche Schlüssel, der berechtigt ist, Admin-Nachrichten an diesen Knoten zu senden." } }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "La clave pública principal autorizada para enviar mensajes de administrador a este nodo." - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -42643,12 +36646,6 @@ "value" : "Die Region, in der du deine Funkgeräte verwenden wirst." } }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "La región donde utilizará sus radios." - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -42683,12 +36680,6 @@ }, "The root topic to use for MQTT." : { "localizations" : { - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "El tema raíz que se utilizará para MQTT." - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -42723,12 +36714,6 @@ }, "The Router roles are only for high vantage locations like mountaintops and towers with few nearby nodes, not for use in urban areas. Improper use will hurt your local mesh." : { "localizations" : { - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Las funciones de enrutador son solo para ubicaciones estratégicas, como cimas de montañas y torres con pocos nodos cercanos, no para uso en áreas urbanas. El uso inadecuado dañará su malla local." - } - }, "sr" : { "stringUnit" : { "state" : "translated", @@ -42745,12 +36730,6 @@ "value" : "Der zweite öffentliche Schlüssel, der berechtigt ist, Admin-Nachrichten an diesen Knoten zu senden." } }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "La clave pública secundaria autorizada para enviar mensajes de administrador a este nodo." - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -42791,12 +36770,6 @@ "value" : "Status der LED (an/aus)" } }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "El estado del LED (encendido/apagado)" - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -42831,12 +36804,6 @@ "value" : "Der dritte öffentliche Schlüssel, der berechtigt ist, Admin-Nachrichten an diesen Knoten zu senden." } }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "La clave pública terciaria autorizada para enviar mensajes de administrador a este nodo." - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -42871,12 +36838,6 @@ }, "The URL for the channel settings" : { "localizations" : { - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "La URL para la configuración del canal." - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -42905,12 +36866,6 @@ }, "The URL for the node to add" : { "localizations" : { - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "La URL del nodo a agregar." - } - }, "ja" : { "stringUnit" : { "state" : "translated", @@ -42933,12 +36888,6 @@ }, "There has been no response to a request for device metadata via PKC admin for this node." : { "localizations" : { - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "No ha habido respuesta a una solicitud de metadatos del dispositivo a través del administrador de PKC para este nodo." - } - }, "sr" : { "stringUnit" : { "state" : "translated", @@ -42949,12 +36898,6 @@ }, "There is an issue with this contact's public key." : { "localizations" : { - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Hay un problema con la clave pública de este contacto." - } - }, "sr" : { "stringUnit" : { "state" : "translated", @@ -42970,12 +36913,6 @@ "These settings will %@ channels. The current LoRa Config will be replaced, if there are substantial changes to the LoRa config the device will reboot" : { "extractionState" : "stale", "localizations" : { - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Estas configuraciones %@ canales. La configuración LoRa actual será reemplazada; si hay cambios sustanciales en la configuración LoRa, el dispositivo se reiniciará" - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -43010,12 +36947,6 @@ "value" : "Dreißig Minuten" } }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "treinta minutos" - } - }, "fr" : { "stringUnit" : { "state" : "translated", @@ -43080,12 +37011,6 @@ "value" : "Dreißig Sekunden" } }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "treinta segundos" - } - }, "fr" : { "stringUnit" : { "state" : "translated", @@ -43150,12 +37075,6 @@ "value" : "Sechsunddreissig Stunden" } }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "treinta y seis horas" - } - }, "fr" : { "stringUnit" : { "state" : "translated", @@ -43214,12 +37133,6 @@ }, "This conversation will be deleted." : { "localizations" : { - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Esta conversación será eliminada." - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -43248,12 +37161,6 @@ }, "This could take a while, response will appear in the trace route log for the node it was sent to." : { "localizations" : { - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Esto podría tardar un poco; la respuesta aparecerá en el registro de ruta de seguimiento del nodo al que se envió." - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -43282,12 +37189,6 @@ }, "This device will send out range test messages on the selected interval." : { "localizations" : { - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Este dispositivo enviará mensajes de prueba de alcance en el intervalo seleccionado." - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -43328,12 +37229,6 @@ "value" : "Diese Nachricht wurde höchstwahrscheinlich nicht übermittelt." } }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Es probable que este mensaje no se haya entregado." - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -43362,12 +37257,6 @@ }, "This node does not support any configurable modules." : { "localizations" : { - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Este nodo no admite ningún módulo configurable." - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -43396,12 +37285,6 @@ }, "This will disable fixed position and remove the currently set position." : { "localizations" : { - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Esto desactivará la posición fija y eliminará la posición establecida actualmente." - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -43430,12 +37313,6 @@ }, "This will send a current position from your phone and enable fixed position." : { "localizations" : { - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Esto enviará una posición actual desde su teléfono y habilitará la posición fija." - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -43476,12 +37353,6 @@ "value" : "Drei Stunden" } }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "tres horas" - } - }, "fr" : { "stringUnit" : { "state" : "translated", @@ -43546,12 +37417,6 @@ "value" : "Drei Sekunden" } }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "tres segundos" - } - }, "fr" : { "stringUnit" : { "state" : "translated", @@ -43616,12 +37481,6 @@ "value" : "Daumen runter" } }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Pulgar hacia abajo" - } - }, "fr" : { "stringUnit" : { "state" : "translated", @@ -43686,12 +37545,6 @@ "value" : "Daumen hoch" } }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Pulgares hacia arriba" - } - }, "fr" : { "stringUnit" : { "state" : "translated", @@ -43756,12 +37609,6 @@ "value" : "Zeit" } }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Tiempo" - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -43796,12 +37643,6 @@ "value" : "Zeitstempel" } }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Marca de tiempo" - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -43836,12 +37677,6 @@ "value" : "Zeitzone" } }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Huso horario" - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -43876,12 +37711,6 @@ "value" : "Zeitzone für Daten auf dem Gerätebildschirm und Log." } }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Zona horaria para fechas en la pantalla del dispositivo y registro." - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -43916,12 +37745,6 @@ "value" : "Zeitlimit erreicht" } }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Se acabó el tiempo" - } - }, "fr" : { "stringUnit" : { "state" : "translated", @@ -43986,12 +37809,6 @@ "value" : "Zeitstempel" } }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Marca de tiempo" - } - }, "fr" : { "stringUnit" : { "state" : "translated", @@ -44050,12 +37867,6 @@ }, "Timing and Overrides" : { "localizations" : { - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Temporización y anulaciones" - } - }, "sr" : { "stringUnit" : { "state" : "translated", @@ -44066,12 +37877,6 @@ }, "TLS Enabled" : { "localizations" : { - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "TLS habilitado" - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -44106,12 +37911,6 @@ }, "To comply with privacy laws like CCPA and GDPR, we avoid sharing exact location data. Instead, we use anonymized or approximate (imprecise) location information to protect your privacy." : { "localizations" : { - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Para cumplir con las leyes de privacidad como CCPA y GDPR, evitamos compartir datos de ubicación exacta. En su lugar, utilizamos información de ubicación anónima o aproximada (imprecisa) para proteger su privacidad." - } - }, "ja" : { "stringUnit" : { "state" : "translated", @@ -44134,12 +37933,6 @@ }, "To Radio (TX): %lld" : { "localizations" : { - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "A radio (TX): %lld" - } - }, "sr" : { "stringUnit" : { "state" : "translated", @@ -44153,12 +37946,6 @@ }, "Topic: %@" : { "localizations" : { - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Tema: %@" - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -44193,12 +37980,6 @@ "value" : "Total" } }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Total" - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -44227,12 +38008,6 @@ }, "Total PAX" : { "localizations" : { - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "PAX TOTALES" - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -44267,12 +38042,6 @@ }, "Trace Route" : { "localizations" : { - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Ruta de seguimiento" - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -44301,12 +38070,6 @@ }, "Trace Route (in %@s)" : { "localizations" : { - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Ruta de seguimiento (en %@s)" - } - }, "ja" : { "stringUnit" : { "state" : "translated", @@ -44329,12 +38092,6 @@ }, "Trace Route Log" : { "localizations" : { - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Registro de ruta de seguimiento" - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -44375,12 +38132,6 @@ "value" : "Traceroute Ergebnis: %@" } }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Solicitud de ruta de seguimiento devuelta: %@" - } - }, "fr" : { "stringUnit" : { "state" : "translated", @@ -44439,12 +38190,6 @@ }, "Trace Route Sent" : { "localizations" : { - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Ruta de seguimiento enviada" - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -44473,12 +38218,6 @@ }, "Trace route sent to %@" : { "localizations" : { - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Ruta de seguimiento enviada a %@" - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -44507,12 +38246,6 @@ }, "Trace route to %@ was not sent." : { "localizations" : { - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "La ruta de seguimiento a %@ no se envió." - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -44541,12 +38274,6 @@ }, "Trace Route was rate limited. You can send a trace route a maximum of once every thirty seconds." : { "localizations" : { - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Trace Route tenía una tarifa limitada. Puede enviar una ruta de rastreo como máximo una vez cada treinta segundos." - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -44581,12 +38308,6 @@ "value" : "Standorte verfolgen und teilen" } }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Seguimiento y compartir ubicaciones" - } - }, "sr" : { "stringUnit" : { "state" : "translated", @@ -44597,12 +38318,6 @@ }, "Tracker" : { "localizations" : { - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Rastreador" - } - }, "ja" : { "stringUnit" : { "state" : "translated", @@ -44637,12 +38352,6 @@ "value" : "Verkehr" } }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Tráfico" - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -44671,12 +38380,6 @@ }, "Transmit data (txd) GPIO pin" : { "localizations" : { - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Transmitir datos (txd) pin GPIO" - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -44705,12 +38408,6 @@ }, "Transmit Enabled" : { "localizations" : { - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Transmisión habilitada" - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -44745,12 +38442,6 @@ }, "Treat double tap on supported accelerometers as a user button press." : { "localizations" : { - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Considere el doble toque en los acelerómetros compatibles como si el usuario presionara un botón." - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -44785,12 +38476,6 @@ }, "TriggerType" : { "localizations" : { - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Tipo de disparador" - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -44819,12 +38504,6 @@ }, "Triple Click Ad Hoc Ping" : { "localizations" : { - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Ping ad hoc de triple clic" - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -44859,12 +38538,6 @@ "value" : "Erneut versuchen" } }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Intentar otra vez" - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -44899,12 +38572,6 @@ "value" : "Zwölf Stunden" } }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Doce horas" - } - }, "fr" : { "stringUnit" : { "state" : "translated", @@ -44969,12 +38636,6 @@ "value" : "Vierundzwanzig Stunden" } }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "veinticuatro horas" - } - }, "fr" : { "stringUnit" : { "state" : "translated", @@ -45039,12 +38700,6 @@ "value" : "Zwei Stunden" } }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "dos horas" - } - }, "fr" : { "stringUnit" : { "state" : "translated", @@ -45109,12 +38764,6 @@ "value" : "Zwei Minutes" } }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "dos minutos" - } - }, "fr" : { "stringUnit" : { "state" : "translated", @@ -45179,12 +38828,6 @@ "value" : "Zwei Sekunden" } }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "dos segundos" - } - }, "fr" : { "stringUnit" : { "state" : "translated", @@ -45243,12 +38886,6 @@ }, "UDP Broadcast" : { "localizations" : { - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Transmisión UDP" - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -45277,12 +38914,6 @@ }, "Ukraine 433MHz" : { "localizations" : { - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Ucrania 433MHz" - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -45311,12 +38942,6 @@ }, "Ukraine 868MHz" : { "localizations" : { - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Ucrania 868MHz" - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -45345,12 +38970,6 @@ }, "Un-Favorite" : { "localizations" : { - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "No favorito" - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -45379,12 +38998,6 @@ }, "Unhealthy" : { "localizations" : { - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Malsano" - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -45413,12 +39026,6 @@ }, "Unhealthy for Sensitive Groups" : { "localizations" : { - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "No saludable para grupos sensibles" - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -45447,12 +39054,6 @@ }, "United States" : { "localizations" : { - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Estados Unidos" - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -45481,12 +39082,6 @@ }, "Units displayed on the device screen" : { "localizations" : { - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Unidades mostradas en la pantalla del dispositivo." - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -45515,12 +39110,6 @@ }, "unknown" : { "localizations" : { - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "desconocido" - } - }, "it" : { "stringUnit" : { "state" : "needs_review", @@ -45549,12 +39138,6 @@ }, "Unknown" : { "localizations" : { - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Desconocido" - } - }, "fr" : { "stringUnit" : { "state" : "translated", @@ -45619,12 +39202,6 @@ "value" : "Unbekanntes alter" } }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Edad desconocida" - } - }, "fr" : { "stringUnit" : { "state" : "translated", @@ -45689,12 +39266,6 @@ "value" : "Nicht benachrichtigbar" } }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "inmensable" - } - }, "ja" : { "stringUnit" : { "state" : "translated", @@ -45711,12 +39282,6 @@ }, "Unmonitored" : { "localizations" : { - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "No monitoreado" - } - }, "ja" : { "stringUnit" : { "state" : "translated", @@ -45739,12 +39304,6 @@ "value" : "Unset" } }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Desarmado" - } - }, "fr" : { "stringUnit" : { "state" : "translated", @@ -45803,12 +39362,6 @@ }, "Unsupported" : { "localizations" : { - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "No compatible" - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -45843,12 +39396,6 @@ "value" : "Hoch" } }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Arriba" - } - }, "fr" : { "stringUnit" : { "state" : "translated", @@ -45907,12 +39454,6 @@ }, "Up Down 1" : { "localizations" : { - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "arriba abajo 1" - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -45945,24 +39486,8 @@ } } }, - "UPDATE IN" : { - "localizations" : { - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "ACTUALIZAR EN" - } - } - } - }, "Update Interval" : { "localizations" : { - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Intervalo de actualización" - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -45997,12 +39522,6 @@ "value" : "Firmware aktualisieren" } }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Actualice su firmware" - } - }, "fr" : { "stringUnit" : { "state" : "translated", @@ -46061,12 +39580,6 @@ }, "Updated Node Stats Data." : { "localizations" : { - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Datos de estadísticas de nodos actualizados." - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -46101,12 +39614,6 @@ "value" : "Aktualisiert: %@" } }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Actualizado: %@" - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -46135,12 +39642,6 @@ }, "Uplink Enabled" : { "localizations" : { - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Enlace ascendente habilitado" - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -46181,12 +39682,6 @@ "value" : "Hochladen fehlgeschlagen" } }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Error de carga" - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -46227,12 +39722,6 @@ "value" : "Lade GeoJSON-Dateien hoch, um eigene Karten-Overlays anzuzeigen. Die Dateien werden lokal gespeichert und dürfen bis zu 10 MB groß sein." } }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Cargue archivos GeoJSON para mostrar superposiciones de mapas personalizados. Los archivos se almacenan localmente y pueden tener hasta 10 MB." - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -46268,12 +39757,6 @@ "Upload Map Data" : { "comment" : "Title for map data upload screen", "localizations" : { - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Cargar datos del mapa" - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -46315,12 +39798,6 @@ "value" : "Lade Kartendaten hoch, um Overlays zu aktivieren" } }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Cargar datos de mapas para habilitar superposiciones" - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -46361,12 +39838,6 @@ "value" : "Kartendaten hochladen" } }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Cargar superposiciones de mapas" - } - }, "sr" : { "stringUnit" : { "state" : "translated", @@ -46383,12 +39854,6 @@ "value" : "Hochladen erfolgreich" } }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Subir con éxito" - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -46429,12 +39894,6 @@ "value" : "Hochgeladene Kartendaten" } }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Superposiciones de mapas cargados" - } - }, "sr" : { "stringUnit" : { "state" : "translated", @@ -46445,12 +39904,6 @@ }, "Uptime" : { "localizations" : { - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "tiempo de actividad" - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -46497,12 +39950,6 @@ "value" : "Telemetriedaten erfassen" } }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Datos de uso y fallos" - } - }, "sr" : { "stringUnit" : { "state" : "translated", @@ -46513,12 +39960,6 @@ }, "Use a PWM output (like the RAK Buzzer) for tunes instead of an on/off output. This will ignore the output, output duration and active settings and use the device config buzzer GPIO option instead." : { "localizations" : { - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Utilice una salida PWM (como el RAK Buzzer) para melodías en lugar de una salida de encendido/apagado. Esto ignorará la salida, la duración de la salida y la configuración activa y en su lugar utilizará la opción GPIO del zumbador de configuración del dispositivo." - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -46553,12 +39994,6 @@ }, "Use I2S As Buzzer" : { "localizations" : { - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Utilice I2S como zumbador" - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -46593,12 +40028,6 @@ "value" : "Standort verwenden" } }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Usar mi ubicación" - } - }, "ja" : { "stringUnit" : { "state" : "translated", @@ -46621,12 +40050,6 @@ "value" : "Voreinstellung verwenden" } }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Usar preajuste" - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -46661,12 +40084,6 @@ }, "Use PWM Buzzer" : { "localizations" : { - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Usar zumbador PWM" - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -46707,12 +40124,6 @@ "value" : "Verwende das GPS deines Handys anstelle des GPS deines Funkgeräts." } }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Utilice el GPS de su teléfono para enviar ubicaciones a su nodo en lugar de utilizar un GPS de hardware en su nodo." - } - }, "sr" : { "stringUnit" : { "state" : "translated", @@ -46723,12 +40134,6 @@ }, "Used to create a shared key with a remote device." : { "localizations" : { - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Se utiliza para crear una clave compartida con un dispositivo remoto." - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -46763,12 +40168,6 @@ "value" : "Wird verwendet, um nicht überwachte oder Infrastrukturknoten zu identifizieren, damit Nachrichten nicht an Knoten gesendet werden, die niemals antworten werden." } }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Se utiliza para identificar nodos de infraestructura o no supervisados, de modo que la mensajería no esté disponible para nodos que nunca responderán." - } - }, "ja" : { "stringUnit" : { "state" : "translated", @@ -46791,12 +40190,6 @@ "value" : "Benutzer" } }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Usuario" - } - }, "fr" : { "stringUnit" : { "state" : "translated", @@ -46861,12 +40254,6 @@ "value" : "Benutzerkonfiguration" } }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Configuración de usuario" - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -46907,12 +40294,6 @@ "value" : "Benutzerdaten" } }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Detalles del usuario" - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -46947,12 +40328,6 @@ }, "User Id" : { "localizations" : { - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Identificación de usuario" - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -46997,12 +40372,6 @@ "value" : "Daten verfügbar" } }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Usuario subido" - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -47043,12 +40412,6 @@ "value" : "Benutzername" } }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Nombre de usuario" - } - }, "fr" : { "stringUnit" : { "state" : "translated", @@ -47107,12 +40470,6 @@ }, "Uses pullup resistor" : { "localizations" : { - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Utiliza resistencia pullup" - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -47141,12 +40498,6 @@ }, "Utilizes the network connection on your phone to connect to MQTT." : { "localizations" : { - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Utiliza la conexión de red de su teléfono para conectarse a MQTT." - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -47187,12 +40538,6 @@ "value" : "Fahrzeugsteuerkurs" } }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Rumbo del vehículo" - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -47227,12 +40572,6 @@ "value" : "Fahrzeuggeschwindigkeit" } }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Velocidad del vehículo" - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -47261,12 +40600,6 @@ }, "Verify who you are messaging with by comparing public keys in person or over the phone. The most recent public key for this node does not match the previously recorded key. You can delete the node and let it exchange keys again if the key change was due to a factory reset or other intentional action but this also may indicate a more serious security problem." : { "localizations" : { - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Verifique con quién está enviando mensajes comparando claves públicas en persona o por teléfono. La clave pública más reciente para este nodo no coincide con la clave registrada anteriormente. Puede eliminar el nodo y dejar que intercambie claves nuevamente si el cambio de clave se debió a un restablecimiento de fábrica u otra acción intencional, pero esto también puede indicar un problema de seguridad más grave." - } - }, "sr" : { "stringUnit" : { "state" : "translated", @@ -47283,12 +40616,6 @@ "value" : "Version %1$@ includes substantial network optimizations and extensive changes to devices and client apps. Only nodes version %2$@ and above are supported." } }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "La versión %@ incluye optimizaciones sustanciales de la red y cambios extensos en los dispositivos y aplicaciones cliente. Solo se admiten nodos de versión %@ y superiores." - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -47329,12 +40656,6 @@ "value" : "Version: %1$@ (%2$@)" } }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Versión: %@ (%@)" - } - }, "ja" : { "stringUnit" : { "state" : "translated", @@ -47363,12 +40684,6 @@ "value" : "Version: %1$@ (%2$@) " } }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Versión: %1$@ (%2$@)" - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -47403,12 +40718,6 @@ }, "Very Unhealthy" : { "localizations" : { - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "muy poco saludable" - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -47443,12 +40752,6 @@ "value" : "Via Lora" } }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Vía Lora" - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -47483,12 +40786,6 @@ "value" : "Via Mqtt" } }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Vía Mqtt" - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -47529,12 +40826,6 @@ "value" : "Voltage" } }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Voltaje" - } - }, "fr" : { "stringUnit" : { "state" : "translated", @@ -47593,12 +40884,6 @@ }, "Volts %@" : { "localizations" : { - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Voltios %@" - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -47633,12 +40918,6 @@ "value" : "Warte..." } }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Espera" - } - }, "fr" : { "stringUnit" : { "state" : "translated", @@ -47697,12 +40976,6 @@ }, "Waiting to be acknowledged. . ." : { "localizations" : { - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Esperando ser reconocido. . ." - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -47731,12 +41004,6 @@ }, "Wake Screen on tap or motion" : { "localizations" : { - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Activar pantalla con un toque o movimiento" - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -47771,12 +41038,6 @@ "value" : "Gehen" } }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Caminando" - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -47817,12 +41078,6 @@ "value" : "Welle" } }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Ola" - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -47857,12 +41112,6 @@ }, "Waypoint Failed to Send" : { "localizations" : { - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "El punto de referencia no se pudo enviar" - } - }, "ja" : { "stringUnit" : { "state" : "translated", @@ -47885,12 +41134,6 @@ "value" : "Wegpunktoptionen" } }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Opciones de punto de referencia" - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -47925,12 +41168,6 @@ "value" : "Wegpunkt von Knoten empfangen: %@" } }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Paquete de waypoint recibido del nodo: %@" - } - }, "fr" : { "stringUnit" : { "state" : "translated", @@ -47988,30 +41225,8 @@ } }, "Waypoints" : { - "localizations" : { - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Puntos de referencia" - } - } - } + }, -<<<<<<< Updated upstream -======= - "We anonymously collect usage and crash data to improve the app. This helps us understand how the app is being used and where we can make improvements. The data we collect is non-personally identifiable and cannot be linked to you as an individual. You can opt out of this under app settings." : { - "comment" : "A description of how the app collects and uses user data. Includes a link to the app settings.", - "isCommentAutoGenerated" : true, - "localizations" : { - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Recopilamos de forma anónima datos de uso y fallos para mejorar la aplicación. Esto nos ayuda a comprender cómo se utiliza la aplicación y dónde podemos realizar mejoras. Los datos que recopilamos no son identificables personalmente y no pueden vincularse a usted como individuo. Puede optar por no participar en la configuración de la aplicación." - } - } - } - }, ->>>>>>> Stashed changes "Weather Conditions" : { "localizations" : { "de" : { @@ -48020,12 +41235,6 @@ "value" : "Wetterverhältnisse" } }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Condiciones climáticas" - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -48060,12 +41269,6 @@ }, "Web Flasher" : { "localizations" : { - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Intermitente web" - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -48094,12 +41297,6 @@ }, "Website" : { "localizations" : { - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Sitio web" - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -48134,12 +41331,6 @@ }, "Weight" : { "localizations" : { - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Peso" - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -48174,12 +41365,6 @@ "value" : "Willkommen bei" } }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Bienvenido a" - } - }, "sr" : { "stringUnit" : { "state" : "translated", @@ -48196,12 +41381,6 @@ "value" : "Was bedeutet das Schloß?" } }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "¿Qué significa la cerradura?" - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -48242,12 +41421,6 @@ "value" : "Was ist Meshtastic?" } }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "¿Qué es Meshtastic?" - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -48282,12 +41455,6 @@ }, "What licensed operator mode does:\n* Sets the node name to your call sign \n* Broadcasts node info every 10 minutes \n* Overrides frequency, dutycycle and tx power \n* Disables encryption" : { "localizations" : { - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Qué hace el modo de operador con licencia:\n* Establece el nombre del nodo según su indicativo de llamada \n* Transmite información del nodo cada 10 minutos \n* Anula la frecuencia, el ciclo de trabajo y la potencia de transmisión. \n* Desactiva el cifrado" - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -48322,12 +41489,6 @@ }, "When enabled the PAX Counter module counts the number of people passing by using WiFi and Bluetooth. Both WiFI and Bluetooth must be disabled for PAX counter to work." : { "localizations" : { - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Cuando está habilitado, el módulo Contador de PAX cuenta el número de personas que pasan mediante WiFi y Bluetooth. Tanto WiFI como Bluetooth deben estar desactivados para que funcione el contador de PAX." - } - }, "he" : { "stringUnit" : { "state" : "translated", @@ -48374,12 +41535,6 @@ }, "When using in GPIO mode, keep the output on for this long. " : { "localizations" : { - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Cuando lo use en modo GPIO, mantenga la salida encendida durante este tiempo." - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -48414,12 +41569,6 @@ }, "Whether or not use INPUT_PULLUP mode for GPIO pin. Only applicable if the board uses pull-up resistors on the pin" : { "localizations" : { - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Si se utiliza o no el modo INPUT_PULLUP para el pin GPIO. Solo aplicable si la placa usa resistencias pull-up en el pin" - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -48448,12 +41597,6 @@ }, "WiFi" : { "localizations" : { - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Wi-Fi" - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -48494,12 +41637,6 @@ "value" : "WiFi Optionen" } }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Opciones WiFi" - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -48528,12 +41665,6 @@ }, "Will sleep everything as much as possible, for the tracker and sensor role this will also include the lora radio. Don't use this setting if you want to use your device with the phone apps or are using a device without a user button." : { "localizations" : { - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Dormirá todo lo más posible, para la función de rastreador y sensor esto también incluirá la radio lora. No use esta configuración si desea usar su dispositivo con las aplicaciones del teléfono o si está usando un dispositivo sin un botón de usuario." - } - }, "he" : { "stringUnit" : { "state" : "translated", @@ -48586,12 +41717,6 @@ }, "Wind" : { "localizations" : { - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Viento" - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -48626,12 +41751,6 @@ "value" : "Windrichtung" } }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Dirección del viento" - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -48666,12 +41785,6 @@ "value" : "Windgeschwindigkeit" } }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Velocidad del viento" - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -48706,12 +41819,6 @@ "value" : "Innerhalb %@" } }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Dentro %@" - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -48750,12 +41857,6 @@ }, "x" : { "localizations" : { - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "incógnita" - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -48790,12 +41891,6 @@ "value" : "X: %1$@, Y: %2$d" } }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "X: %@, Y: %d" - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -48837,12 +41932,6 @@ "value" : "X: %1$@, Y: %2$f" } }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "X: %@, Y: %f" - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -48884,12 +41973,6 @@ "value" : "X: %1$@, Y: %2$lld" } }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "X: %@, Y: %lld" - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -48925,12 +42008,6 @@ }, "y" : { "localizations" : { - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "y" - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -48975,12 +42052,6 @@ "value" : "Gestern" } }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Ayer" - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -49009,12 +42080,6 @@ }, "You can also update your Meshtastic device over bluetooth using the Nordic DFU app." : { "localizations" : { - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "También puede actualizar su dispositivo Meshtastic a través de bluetooth utilizando la aplicación Nordic DFU." - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -49055,12 +42120,6 @@ "value" : "Du kannst Kanalnachrichten (Gruppenchats) und Direktnachrichten senden und empfangen. Bei jeder Nachricht kannst du lange drücken, um verfügbare Aktionen wie Kopieren, Antworten, Tapback und Löschen sowie Zustelldetails anzuzeigen." } }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Puedes enviar y recibir canales (chats grupales) y mensajes directos. Desde cualquier mensaje, puede mantener presionado para ver las acciones disponibles como copiar, responder, retroceder y eliminar, así como los detalles de entrega." - } - }, "fr" : { "stringUnit" : { "state" : "translated", @@ -49119,12 +42178,6 @@ }, "Your current location will be set as the fixed position and broadcast over the mesh on the position interval." : { "localizations" : { - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Su ubicación actual se establecerá como posición fija y se transmitirá sobre la malla en el intervalo de posición." - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -49165,12 +42218,6 @@ "value" : "Deine Firmware ist aktuell" } }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Su firmware está actualizado" - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -49205,12 +42252,6 @@ }, "Your MQTT Server must support TLS." : { "localizations" : { - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Su servidor MQTT debe admitir TLS." - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -49239,12 +42280,6 @@ }, "Your node will periodically send an unencrypted map report packet to the configured MQTT server, this includes id, short and long name, approximate location, hardware model, role, firmware version, LoRa region, modem preset and primary channel name." : { "localizations" : { - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Su nodo enviará periódicamente un paquete de informe de mapa sin cifrar al servidor MQTT configurado, esto incluye identificación, nombre corto y largo, ubicación aproximada, modelo de hardware, función, versión de firmware, región LoRa, configuración predeterminada del módem y nombre del canal principal." - } - }, "ja" : { "stringUnit" : { "state" : "translated", @@ -49267,12 +42302,6 @@ }, "Your node’s operating frequency is calculated based on the region, modem preset, and this field. When 0, the slot is automatically calculated based on the primary channel name." : { "localizations" : { - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "La frecuencia operativa de su nodo se calcula en función de la región, la configuración predeterminada del módem y este campo. Cuando es 0, la ranura se calcula automáticamente en función del nombre del canal principal." - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -49301,12 +42330,6 @@ }, "Your position has been sent with a request for a response with their position. You will receive a notification when a position is returned." : { "localizations" : { - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Su posición ha sido enviada con una solicitud de respuesta con su posición. Recibirá una notificación cuando se devuelva una posición." - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -49340,23 +42363,10 @@ } }, "Your public key is generated from your private key and sent to other nodes on the mesh so they can compute a shared secret key with you." : { - "localizations" : { - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Su clave pública se genera a partir de su clave privada y se envía a otros nodos de la malla para que puedan calcular una clave secreta compartida con usted." - } - } - } + }, "Your region has a %lld%% duty cycle. MQTT is not advised when you are duty cycle restricted, the extra traffic will quickly overwhelm your LoRa mesh." : { "localizations" : { - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Su región tiene un ciclo de trabajo %lld%%. No se recomienda MQTT cuando tiene un ciclo de trabajo restringido, el tráfico adicional abrumará rápidamente su malla LoRa." - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -49391,12 +42401,6 @@ }, "Your region has a %lld%% hourly duty cycle, your radio will stop sending packets when it reaches the hourly limit." : { "localizations" : { - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Su región tiene un ciclo de trabajo por hora %lld%%, su radio dejará de enviar paquetes cuando alcance el límite por hora." - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -49425,12 +42429,6 @@ }, "Your route file must have both Latitude and Longitude columns and headers." : { "localizations" : { - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Su archivo de ruta debe tener columnas y encabezados de Latitud y Longitud." - } - }, "it" : { "stringUnit" : { "state" : "translated", From 3d8887bbe9598564b4d204c919cc5bdd98fde6a5 Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Sun, 4 Jan 2026 20:56:32 -0800 Subject: [PATCH 62/68] Revert "update the translations (#1540)" (#1544) This reverts commit cb2fd8cc15185f6b9ce8a940d8ca8d11a32a2f80. --- Localizable.xcstrings | 120 ++---------------------------------------- 1 file changed, 3 insertions(+), 117 deletions(-) diff --git a/Localizable.xcstrings b/Localizable.xcstrings index b47fe554..bb6de690 100644 --- a/Localizable.xcstrings +++ b/Localizable.xcstrings @@ -2114,12 +2114,6 @@ "value" : "О Мештастику" } }, - "zh-Hans" : { - "stringUnit" : { - "state" : "translated", - "value" : "关于" - } - }, "zh-Hant-TW" : { "stringUnit" : { "state" : "translated", @@ -7610,12 +7604,6 @@ "value" : "Очисти логове" } }, - "zh-Hans" : { - "stringUnit" : { - "state" : "translated", - "value" : "清除Log记录" - } - }, "zh-Hant-TW" : { "stringUnit" : { "state" : "translated", @@ -8334,13 +8322,7 @@ "state" : "translated", "value" : "Повежи се" } - }, - "zh-Hans" : { - "stringUnit" : { - "state" : "translated", - "value" : "连接" - } - }, + } } }, "Connect to a Node" : { @@ -10354,12 +10336,6 @@ "value" : "Логови сензора откривања" } }, - "zh-Hans" : { - "stringUnit" : { - "state" : "translated", - "value" : "检测传感器记录" - } - }, "zh-Hant-TW" : { "stringUnit" : { "state" : "translated", @@ -14649,12 +14625,6 @@ "value" : "Пронађи контакт" } }, - "zh-Hans" : { - "stringUnit" : { - "state" : "translated", - "value" : "搜索联系人" - } - }, "zh-Hant-TW" : { "stringUnit" : { "state" : "translated", @@ -14689,12 +14659,6 @@ "value" : "Пронађи чвор" } }, - "zh-Hans" : { - "stringUnit" : { - "state" : "translated", - "value" : "搜索节点" - } - }, "zh-Hant-TW" : { "stringUnit" : { "state" : "translated", @@ -14982,12 +14946,6 @@ "value" : "Прво откривање" } }, - "zh-Hans" : { - "stringUnit" : { - "state" : "translated", - "value" : "首次通信" - } - }, "zh-Hant-TW" : { "stringUnit" : { "state" : "translated", @@ -15980,12 +15938,6 @@ "value" : "Апсолутна подршка" } }, - "zh-Hans" : { - "stringUnit" : { - "state" : "translated", - "value" : "官方支持" - } - }, "zh-Hant-TW" : { "stringUnit" : { "state" : "translated", @@ -16936,12 +16888,6 @@ "value" : "Сакриј упозорења" } }, - "zh-Hans" : { - "stringUnit" : { - "state" : "translated", - "value" : "通知静音" - } - }, "zh-Hant-TW" : { "stringUnit" : { "state" : "translated", @@ -20450,13 +20396,7 @@ "state" : "translated", "value" : "Ручно" } - }, - "zh-Hans" : { - "stringUnit" : { - "state" : "translated", - "value" : "更多" - } - }, + } } }, "Manual Configuration" : { @@ -20983,7 +20923,7 @@ "zh-Hans" : { "stringUnit" : { "state" : "translated", - "value" : "地图" + "value" : "Mesh 地图" } }, "zh-Hant-TW" : { @@ -23844,12 +23784,6 @@ "value" : "Број чвора" } }, - "zh-Hans" : { - "stringUnit" : { - "state" : "translated", - "value" : "用户编号" - } - }, "zh-Hant-TW" : { "stringUnit" : { "state" : "translated", @@ -26663,12 +26597,6 @@ "value" : "Логови позиција" } }, - "zh-Hans" : { - "stringUnit" : { - "state" : "translated", - "value" : "位置记录" - } - }, "zh-Hant-TW" : { "stringUnit" : { "state" : "translated", @@ -27033,12 +26961,6 @@ "value" : "Логови метрике снаге" } }, - "zh-Hans" : { - "stringUnit" : { - "state" : "translated", - "value" : "电源指标记录" - } - }, "zh-Hant-TW" : { "stringUnit" : { "state" : "translated", @@ -30019,12 +29941,6 @@ "value" : "Улога" } }, - "zh-Hans" : { - "stringUnit" : { - "state" : "translated", - "value" : "节点类型" - } - }, "zh-Hant-TW" : { "stringUnit" : { "state" : "translated", @@ -30261,12 +30177,6 @@ "value" : "Снимач руте" } }, - "zh-Hans" : { - "stringUnit" : { - "state" : "translated", - "value" : "路线记录" - } - }, "zh-Hant-TW" : { "stringUnit" : { "state" : "translated", @@ -30443,12 +30353,6 @@ "value" : "Руте" } }, - "zh-Hans" : { - "stringUnit" : { - "state" : "translated", - "value" : "路线" - } - }, "zh-Hant-TW" : { "stringUnit" : { "state" : "translated", @@ -31665,12 +31569,6 @@ "value" : "Изабери тип разговора" } }, - "zh-Hans" : { - "stringUnit" : { - "state" : "translated", - "value" : "请选择对话类型" - } - }, "zh-Hant-TW" : { "stringUnit" : { "state" : "translated", @@ -38110,12 +38008,6 @@ "value" : "Лог праћења руте комуникације" } }, - "zh-Hans" : { - "stringUnit" : { - "state" : "translated", - "value" : "追踪路由(Trace Route)记录" - } - }, "zh-Hant-TW" : { "stringUnit" : { "state" : "translated", @@ -39928,12 +39820,6 @@ "value" : "Време рада" } }, - "zh-Hans" : { - "stringUnit" : { - "state" : "translated", - "value" : "上线时长" - } - }, "zh-Hant-TW" : { "stringUnit" : { "state" : "translated", From 87412c4c2a6285415f6deab0c2c267e008355aa3 Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Sun, 4 Jan 2026 20:58:43 -0800 Subject: [PATCH 63/68] Revert "NFC Tag contact (#1537)" (#1545) This reverts commit 5c22b8b6e0176f4927bfc79234dabe109b215edf. --- Localizable.xcstrings | 17 +-- Meshtastic/Info.plist | 2 - Meshtastic/Meshtastic.entitlements | 4 - Meshtastic/Router/NavigationState.swift | 1 - Meshtastic/Views/Settings/Settings.swift | 9 -- Meshtastic/Views/Settings/Tools.swift | 164 ----------------------- 6 files changed, 1 insertion(+), 196 deletions(-) delete mode 100644 Meshtastic/Views/Settings/Tools.swift diff --git a/Localizable.xcstrings b/Localizable.xcstrings index bb6de690..c6603edc 100644 --- a/Localizable.xcstrings +++ b/Localizable.xcstrings @@ -9143,10 +9143,6 @@ } } }, - "Create Node Contact NFC Tag" : { - "comment" : "A section header that instructs the user to create a contact NFC tag.", - "isCommentAutoGenerated" : true - }, "Create Waypoint" : { "localizations" : { "de" : { @@ -23754,10 +23750,6 @@ } } }, - "Node Name: %@" : { - "comment" : "A text label displaying the name of the connected node.", - "isCommentAutoGenerated" : true - }, "Node Number" : { "localizations" : { "de" : { @@ -37838,9 +37830,6 @@ } } } - }, - "Tools" : { - }, "Topic: %@" : { "localizations" : { @@ -41737,10 +41726,6 @@ } } }, - "Write Contact to NFC Tag" : { - "comment" : "A button that writes a contact to an NFC tag.", - "isCommentAutoGenerated" : true - }, "x" : { "localizations" : { "it" : { @@ -42343,4 +42328,4 @@ } }, "version" : "1.1" -} \ No newline at end of file +} diff --git a/Meshtastic/Info.plist b/Meshtastic/Info.plist index c2cbcdd8..863fb0e9 100644 --- a/Meshtastic/Info.plist +++ b/Meshtastic/Info.plist @@ -97,8 +97,6 @@ LSSupportsOpeningDocumentsInPlace - NFCReaderUsageDescription - We use NFC tags to share node contacts NSBluetoothAlwaysUsageDescription We use bluetooth to connect to nearby Meshtastic Devices NSBluetoothPeripheralUsageDescription diff --git a/Meshtastic/Meshtastic.entitlements b/Meshtastic/Meshtastic.entitlements index a35e74ee..4dbdb836 100644 --- a/Meshtastic/Meshtastic.entitlements +++ b/Meshtastic/Meshtastic.entitlements @@ -9,10 +9,6 @@ com.apple.developer.carplay-communication - com.apple.developer.nfc.readersession.formats - - TAG - com.apple.developer.usernotifications.critical-alerts com.apple.developer.weatherkit diff --git a/Meshtastic/Router/NavigationState.swift b/Meshtastic/Router/NavigationState.swift index 8c2ff6b3..48a97b93 100644 --- a/Meshtastic/Router/NavigationState.swift +++ b/Meshtastic/Router/NavigationState.swift @@ -52,7 +52,6 @@ enum SettingsNavigationState: String { case debugLogs case appFiles case firmwareUpdates - case tools } struct NavigationState: Hashable { diff --git a/Meshtastic/Views/Settings/Settings.swift b/Meshtastic/Views/Settings/Settings.swift index 25b0e75a..d3d15a66 100644 --- a/Meshtastic/Views/Settings/Settings.swift +++ b/Meshtastic/Views/Settings/Settings.swift @@ -355,13 +355,6 @@ struct Settings: View { Image(systemName: "gearshape") } } - NavigationLink(value: SettingsNavigationState.tools) { - Label { - Text("Tools") - } icon: { - Image(systemName: "hammer") - } - } NavigationLink(value: SettingsNavigationState.routes) { Label { Text("Routes") @@ -528,8 +521,6 @@ struct Settings: View { AppData() case .firmwareUpdates: Firmware(node: node) - case .tools: - Tools() } } .onChange(of: UserDefaults.preferredPeripheralNum ) { _, newConnectedNode in diff --git a/Meshtastic/Views/Settings/Tools.swift b/Meshtastic/Views/Settings/Tools.swift deleted file mode 100644 index 75e439de..00000000 --- a/Meshtastic/Views/Settings/Tools.swift +++ /dev/null @@ -1,164 +0,0 @@ -// -// Tools.swift -// Meshtastic -// -// Created by Benjamin Faershtein on 12/31/25. -// - -import SwiftUI -import CoreNFC -import MeshtasticProtobufs -import OSLog - -struct Tools: View { - @EnvironmentObject var accessoryManager: AccessoryManager - @Environment(\.managedObjectContext) var context - - @StateObject private var nfcReader = NFCReader() - - var connectedNode: NodeInfoEntity? { - if let num = accessoryManager.activeDeviceNum { - return getNodeInfo(id: num, context: context) - } - return nil - } - - var qrString: String { - var contact = SharedContact() - contact.nodeNum = UInt32(connectedNode?.num ?? 0) - contact.user = connectedNode?.toProto().user ?? User() - contact.manuallyVerified = true - - do { - let contactString = try contact.serializedData().base64EncodedString() - return "https://meshtastic.org/v/#" + contactString.base64ToBase64url() - } catch { - Logger.services.error("Error serializing contact: \(error)") - return "" - } - } - - var body: some View { - VStack{ - List { - Section(header: Text("Create Node Contact NFC Tag")) { - if let node = connectedNode { - Text("Node Name: \(node.user?.longName ?? "Unknown")") - - Button { - nfcReader.scan(theActualData: qrString) - } label: { - Label("Write Contact to NFC Tag", systemImage: "tag") - } - .disabled(qrString.isEmpty) - } - } - } - } - .navigationTitle("Tools") - .navigationBarTitleDisplayMode(.inline) - } -} - -#Preview { - Tools() -} - -final class NFCReader: NSObject, ObservableObject, NFCNDEFReaderSessionDelegate { - - private let logger = Logger(subsystem: "org.meshtastic.app", category: "NFC") - private var payloadString = "" - private var session: NFCNDEFReaderSession? - - func scan(theActualData: String) { - payloadString = theActualData - - session = NFCNDEFReaderSession( - delegate: self, - queue: nil, - invalidateAfterFirstRead: false - ) - - session?.alertMessage = "Hold your iPhone near the NFC tag." - session?.begin() - } - - func readerSessionDidBecomeActive(_ session: NFCNDEFReaderSession) { - logger.debug("NFC session became active") - } - - func readerSession(_ session: NFCNDEFReaderSession, - didInvalidateWithError error: Error) { - logger.error("NFC session invalidated: \(error.localizedDescription)") - } - - func readerSession(_ session: NFCNDEFReaderSession, - didDetectNDEFs messages: [NFCNDEFMessage]) { - } - - func readerSession(_ session: NFCNDEFReaderSession, - didDetect tags: [NFCNDEFTag]) { - - guard tags.count == 1, let tag = tags.first else { - session.alertMessage = "More than one tag detected. Please present only one." - DispatchQueue.global().asyncAfter(deadline: .now() + .milliseconds(500)) { - session.restartPolling() - } - return - } - - session.connect(to: tag) { error in - if let error { - self.logger.error("Failed to connect to tag: \(error.localizedDescription)") - session.alertMessage = "Failed to connect to tag." - session.invalidate() - return - } - - tag.queryNDEFStatus { status, _, error in - if let error { - self.logger.error("Failed to query NDEF status: \(error.localizedDescription)") - session.alertMessage = "Failed to read tag." - session.invalidate() - return - } - - switch status { - case .notSupported: - self.logger.error("Tag does not support NDEF") - session.alertMessage = "Tag does not support NDEF." - session.invalidate() - - case .readOnly: - self.logger.error("Tag is read-only") - session.alertMessage = "Tag is read-only." - session.invalidate() - - case .readWrite: - guard let payload = - NFCNDEFPayload.wellKnownTypeURIPayload( - string: self.payloadString - ) else { - self.logger.error("Invalid NDEF payload") - session.alertMessage = "Invalid payload." - session.invalidate() - return - } - - let message = NFCNDEFMessage(records: [payload]) - - tag.writeNDEF(message) { error in - if let error { - self.logger.error("Failed to write NDEF: \(error.localizedDescription)") - session.alertMessage = "Failed to write tag." - } else { - self.logger.info("Successfully wrote NFC tag") - session.alertMessage = "NFC tag written successfully." - } - session.invalidate() - } - } - } - } - } -} From 0d120d057f33d2e6dff359c0bdaacaf253106509 Mon Sep 17 00:00:00 2001 From: Jonathan Bennett Date: Sat, 22 Nov 2025 11:36:23 -0600 Subject: [PATCH 64/68] Update Muzi R1 Neo to actively supported --- Meshtastic/Resources/DeviceHardware.json | 1 + 1 file changed, 1 insertion(+) diff --git a/Meshtastic/Resources/DeviceHardware.json b/Meshtastic/Resources/DeviceHardware.json index 3db1437d..2df81a78 100644 --- a/Meshtastic/Resources/DeviceHardware.json +++ b/Meshtastic/Resources/DeviceHardware.json @@ -1285,3 +1285,4 @@ ] } ] + From 9070f6f4c1ad26cd0496e6efe808ee996ddbd82c Mon Sep 17 00:00:00 2001 From: Ben Meadors Date: Wed, 17 Dec 2025 20:46:41 -0600 Subject: [PATCH 65/68] Revert "Update Muzi R1 Neo to actively supported" --- Meshtastic/Resources/DeviceHardware.json | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/Meshtastic/Resources/DeviceHardware.json b/Meshtastic/Resources/DeviceHardware.json index 2df81a78..893af94f 100644 --- a/Meshtastic/Resources/DeviceHardware.json +++ b/Meshtastic/Resources/DeviceHardware.json @@ -1088,7 +1088,7 @@ "hwModelSlug": "MUZI_R1_NEO", "platformioTarget": "r1-neo", "architecture": "nrf52840", - "activelySupported": true, + "activelySupported": false, "supportLevel": 1, "displayName": "muzi R1 Neo", "tags": [ @@ -1285,4 +1285,3 @@ ] } ] - From 366a1b3327bbf425f9a985232839164eb1812e12 Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Sun, 21 Dec 2025 12:15:01 -0800 Subject: [PATCH 66/68] 2.7.6 Working Changes (#1479) * Bump version * Message list performance fixes into 2.7.6 (#1475) * Remove extra want config call when adding a contact * App badge and unnecessary notification fixes (#1455) * - Fix for app badge not going to zero if a message arrives while you have that chat open - Fix for push notifications popping up when a message is received while that chat is open * Fix for cancelling notifications, works now. And scroll to bottom of conversation upon new message * Fix: Channels help grammer fix (#1452) * remove outdated TCP not available on Apple devices (#1450) * Update initial onboarding view * remove toggle gating for mac * Update crash reporting opt out in real time * Update onboarding text * Use mDNS text records for node name * TCP IP and port on the connection screen * Hide app icon chooser on mac * Infinite loop hang bugfixes and performance improvements for both `UserMessageList` and `ChannelMessageList` (#1465) * 2.7.5 Working Changes (#1460) * Remove extra want config call when adding a contact * App badge and unnecessary notification fixes (#1455) * - Fix for app badge not going to zero if a message arrives while you have that chat open - Fix for push notifications popping up when a message is received while that chat is open * Fix for cancelling notifications, works now. And scroll to bottom of conversation upon new message * Fix: Channels help grammer fix (#1452) * remove outdated TCP not available on Apple devices (#1450) * Update initial onboarding view * remove toggle gating for mac * Update crash reporting opt out in real time * Update onboarding text --------- Co-authored-by: Gnome Adrift <646322+gnomeadrift@users.noreply.github.com> Co-authored-by: Zain Kergaye <62440012+ZainKergaye@users.noreply.github.com> Co-authored-by: NillRudd <102033730+NillRudd@users.noreply.github.com> * UserEntity: add mostRecentMessage and unreadMessages with early exit when lastMessage is nil, and fetch 1 row (not N) otherwise * UserList: replace 5 slow calls to user.messageList with new fast calls * NodeList: always put the connected node at the top of list (if it matches the node filters) * ChannelEntity: add faster mostRecentPrivateMessage and unreadMessages which fetch 1 row (not N) * ChannelList: replace 5 calls to channel.allPrivateMessage with new fast calls * Fix incorrect appState.unreadDirectMessages calculations * MyInfoEntity: also fix unreadMessages count here to be fast, and use it for appState.unreadChannelMessages * UserMessageList: use @FetchRequest to prevent the N^2 behavior that was happening in calls to allPrivateMessages * Refactor ChannelEntityExtension and MyInfoEntityExtension to be more similar to UserEntityExtension * Remove SwiftUI-infinite-loop-causing `.id(redrawTapbacksTrigger)` in ChannelMessageList and UserMessageList (duplicate row ids) * MyInfoEntityExtension: exclude emoji tapbacks (which never get marked as read anyway) from unread message count * Add SaveChannelLinkData so MessageText and MeshtasticApp can use .sheet(item: ...) and avoid infinite loop hang due to Binding rebuild * ChannelMessageList and UserMessageList: switch to stable messageId for ForEach SwiftUI row identity * ChannelMessageList and UserMessageList: debouncedScrollToBottom; keyboardWillShowNotification/keyboardDidShowNotification * ChannelMessageList and UserMessageList: scroll to bottom onFirstAppear * ChannelMessageList and UserMessageList: block spurious markMessagesAsRead when this View is not active --------- Co-authored-by: Garth Vander Houwen Co-authored-by: Gnome Adrift <646322+gnomeadrift@users.noreply.github.com> Co-authored-by: Zain Kergaye <62440012+ZainKergaye@users.noreply.github.com> Co-authored-by: NillRudd <102033730+NillRudd@users.noreply.github.com> * message-list-performance: revert scrolling changes (#1472) * Revert e0f0b4a0f749d2e83946f2c1297e5c97c9fdf46e (ChannelMessageList and UserMessageList: scroll to bottom onFirstAppear) * Revert "ChannelMessageList and UserMessageList: debouncedScrollToBottom; keyboardWillShowNotification/keyboardDidShowNotification" This reverts commit ee1a7c44157eb6970ec2111fc1ac4d67a44a8238. --------- Co-authored-by: Gnome Adrift <646322+gnomeadrift@users.noreply.github.com> Co-authored-by: Zain Kergaye <62440012+ZainKergaye@users.noreply.github.com> Co-authored-by: NillRudd <102033730+NillRudd@users.noreply.github.com> Co-authored-by: Jake-B Co-authored-by: Mike Robbins * Explicitly set unmessagable, seems unnessary * Add back missing mesh map features * Fix: "Retrieving nodes" significantly slower after reconnect extracted from #1424 (#1477) * Fix: "Retrieving nodes" significantly slower after reconnect (#1424) The node database retrieval was calling context.save() for every single NodeInfo packet received (250 saves for 250 nodes). This caused severe performance degradation on reconnect when CoreData had accumulated state. Root Cause: - nodeInfoPacket() called context.save() immediately for each node - With 250 nodes, this meant 250 individual CoreData save operations - On first connection, CoreData is fresh and fast - On reconnect, CoreData has accumulated change tracking, undo management, and memory pressure, making each save progressively slower - This resulted in 10+ second retrieval times vs 1-2 seconds initially Solution: - Added deferSave parameter to nodeInfoPacket() function - During database retrieval (.retrievingDatabase state), defer all saves - Perform a single batch save when database retrieval completes (when NONCE_ONLY_DB configCompleteID is received) - This reduces 250 saves to 1 save Performance Impact: - Eliminates N individual saves during node database sync - Reduces database retrieval time back to 1-2 seconds on reconnect - Matches first-connection performance consistently Fixes #1424 * Revert *MessageListUnified files --------- Co-authored-by: Martin Bogomolni Co-authored-by: Jake-B * Hide route lines filter from mesh map * Mesh Map: fuzz imprecise locations so they're distinguishable and clickable at the highest zoom levels (#1478) Co-authored-by: Garth Vander Houwen * Fix bad merge * Update Meshtastic/Extensions/CoreData/UserEntityExtension.swift Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Update Meshtastic/Views/Nodes/Helpers/Map/MapContent/MeshMapContent.swift Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Update Meshtastic/Extensions/CoreData/ChannelEntityExtension.swift Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Update Meshtastic/Extensions/CoreData/MyInfoEntityExtension.swift Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Update Meshtastic/Extensions/CoreData/ChannelEntityExtension.swift Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Keep list of previous manual connections (#1484) * Keep list of previous manual connections * More descriptive manual connection rows * Merge fixes and new way to show IP on Connect view --------- Co-authored-by: Jake-B * Show who relayed messages (#1486) * Add identification for node that relayed text messages and add count for ammount of relayers of your message * Ack Relays * upsertPositionPacket: don't use future timestamps to set node's lastHeard (#1488) * R1 NEO * Neo * Update Meshtastic/Views/Settings/AppSettings.swift Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Remove bad if * Git rid of extra environment variable * Update Meshtastic/Accessory/Transports/TCP/TCPTransport.swift Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * MeshMap performance: quick wins (#1490) * MeshMap: change onMapCameraChange frequency to .onEnd so that zooming doesn't cause continuous SwiftUI reevaluation on every frame * MeshMapContent: factor out reducedPrecisionMapCircles into a separate function * MeshMapContent: when multiple reducedPrecisionCircles have the same (lat,lon,radius), just draw one (big perf boost in dense areas) * NodeMap performance improvements for high # positions history (#1480) * NodeMapContent: move Route Lines out of ForEach * NodeMapContent: move Convex Hull out of ForEach * NodeMapContent: Replace `position.nodePosition?` with `node` * NodeMapContent: drop unnecessary LazyVStack in showNodeHistory * NodeMapContent: hoist out nodeColorSwift * Move lineCoords, loraCoords calculations within showRouteLines, showConvexHull respectively * Hoist out repeated node.metadata?.positionFlags lookups / PositionFlags creation * NodeMapContent: remove unused @State * NodeMapSwiftUI: add NodeMapContentEquatableWrapper and NodeMapContentSignature to prevent frequent NodeMapContent recomputation and infinite render loops * NodeMapSwiftUI: disable animation during SwiftUI transactions * NodeMapContent: hoist nodeBorderColor and set allowsHitTesting(false) on history point views * NodeMapContent: prerenderHistoryPointCircle and prerenderHistoryPointArrow to avoid thousands of vector draw operations * NodeMapContent: Shared coordinate list for Route Lines and Convex Hull * NodeMapContent.prerenderHistoryPointArrow: add .frame(width: 16, height: 16) * Fix wantRangeTestPackets to correctly follow rangeTestConfig.enabled (#1489) * Fix interval drop down formatter * Clean up channel qr code functionality. * perferredPeripheralId fix * Set opt in * Retry once 5 second timer. dont throw the error * Queue for peripherals * Fix: hoplimit of dms would always fallback to hops away of the node even when configured hops was higher (#1495) * fix hops setting in dms * Fix hops for exchange position * Final fix * Don't favorite client base * Update device hardware * Prevent nil environment metrics * Bump datadog sdk * fix setting device telemetry enabled (#1515) * Update Muzi R1 Neo to actively supported * fix setting device telemetry enabled --------- Co-authored-by: Jonathan Bennett Co-authored-by: Ben Meadors * Don't subscribe to mqtt topic if downlink is not on (#1501) * Dont sub if no downlink * moved reload mqtt connect config * Preview enabled in connected devices (#1509) * Update Muzi R1 Neo to actively supported * Preview enabled in connected devices * Fixing indentation * Fixing indentation --------- Co-authored-by: Jonathan Bennett Co-authored-by: Ben Meadors * UpdateCoreData.updateAnyPacketFrom: mirror firmware's lastHeard/snr/rssi/hopsAway update logic from NodeDB::updateFrom (#1492) * `CLIENT_BASE` add-favorite/role-change confirmation dialog (#1493) * FavoriteNodeButton: refactor task out * AccessoryManager.connectedDeviceRole helper * FavoriteNodeButton: show confirmation dialog when a CLIENT_BASE is trying to add a favorite * addContactFromURL: add comment referencing upcoming change in https://github.com/meshtastic/firmware/pull/8495 * DeviceConfig: role picker: show a warning when selecting CLIENT_BASE, similar to warning shown for ROUTER * Adjust device configuration Client Base warning text * Compass view (#1521) * Added compass view * Added Compass View * Node colors in compass * Update Muzi R1 Neo to actively supported * Update PositionPopover.swift Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Update CompassView.swift Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Update CompassView.swift Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Update Meshtastic/Views/Helpers/CompassView.swift Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Update Meshtastic/Views/Helpers/CompassView.swift Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --------- Co-authored-by: Benjamin Faershtein <119711889+RCGV1@users.noreply.github.com> Co-authored-by: Jonathan Bennett Co-authored-by: Ben Meadors Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Remove discovery queue * revert problematic retry functionalliy * format file * Update & improve zh-Hans translation (#1523) * Update Muzi R1 Neo to actively supported * update & improve zh-Hans translation rt --------- Co-authored-by: Jonathan Bennett Co-authored-by: Ben Meadors * Update protobufs to 2.7.1 * Add long-turbo preset * Disable Range Test module when primary channel is public/unsecured (#1512) * Update Muzi R1 Neo to actively supported * Disable Range Test module when primary channel is public/unsecured Updated RangeTestConfig.swift to determine whether the primary channel (index 0) is operating without encryption or with a 1-byte minimal PSK. Disabled Range Test UI controls when on a public/default channel to prevent user interaction. Added safety enforcement in the save operation: Range Test enabled flag is automatically forced to false before sending updates to the device. Introduced a computed property isPrimaryChannelPublic following existing code patterns and security indicators (e.g., hexDescription PSK length). Matches the behavior implemented in the Android client for consistent policy across platforms. --------- Co-authored-by: Jonathan Bennett Co-authored-by: Ben Meadors * Add new device images * Remove print statement, get rid of cut and paste error --------- Co-authored-by: Gnome Adrift <646322+gnomeadrift@users.noreply.github.com> Co-authored-by: Zain Kergaye <62440012+ZainKergaye@users.noreply.github.com> Co-authored-by: NillRudd <102033730+NillRudd@users.noreply.github.com> Co-authored-by: Jake-B Co-authored-by: Mike Robbins Co-authored-by: Martin Bogomolni Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> Co-authored-by: jake-b <1012393+jake-b@users.noreply.github.com> Co-authored-by: Benjamin Faershtein <119711889+RCGV1@users.noreply.github.com> Co-authored-by: Jonathan Bennett Co-authored-by: Ben Meadors Co-authored-by: Charles Pinesky <25388414+Vaidios@users.noreply.github.com> Co-authored-by: Radio <35003866+radiolee@users.noreply.github.com> Co-authored-by: Jason Houk --- Meshtastic/Resources/DeviceHardware.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Meshtastic/Resources/DeviceHardware.json b/Meshtastic/Resources/DeviceHardware.json index 893af94f..3db1437d 100644 --- a/Meshtastic/Resources/DeviceHardware.json +++ b/Meshtastic/Resources/DeviceHardware.json @@ -1088,7 +1088,7 @@ "hwModelSlug": "MUZI_R1_NEO", "platformioTarget": "r1-neo", "architecture": "nrf52840", - "activelySupported": false, + "activelySupported": true, "supportLevel": 1, "displayName": "muzi R1 Neo", "tags": [ From 2a9f3d571a04b249d8d6b2c1b7613ac432b328d7 Mon Sep 17 00:00:00 2001 From: niccellular <79813408+niccellular@users.noreply.github.com> Date: Mon, 5 Jan 2026 12:40:26 -0500 Subject: [PATCH 67/68] Initial TAK Server implementation for IOS based TAK clients This is my initial implementation for a TAK Server running inside Meshtastic-Apple. --- Localizable.xcstrings | 83 ++- Meshtastic.xcodeproj/project.pbxproj | 73 ++- .../xcshareddata/swiftpm/Package.resolved | 10 +- .../AccessoryManager+FromRadio.swift | 2 + .../AccessoryManager+TAK.swift | 209 ++++++ .../Accessory Manager/AccessoryManager.swift | 4 +- Meshtastic/Extensions/Logger.swift | 3 + Meshtastic/Helpers/Logger.swift | 19 - Meshtastic/Helpers/TAK/CoTMessage.swift | 527 +++++++++++++++ Meshtastic/Helpers/TAK/CoTXMLParser.swift | 343 ++++++++++ Meshtastic/Helpers/TAK/EXICodec.swift | 148 +++++ Meshtastic/Helpers/TAK/FountainCodec.swift | 616 ++++++++++++++++++ .../Helpers/TAK/GenericCoTHandler.swift | 399 ++++++++++++ .../Helpers/TAK/TAKCertificateManager.swift | 589 +++++++++++++++++ Meshtastic/Helpers/TAK/TAKConnection.swift | 496 ++++++++++++++ .../Helpers/TAK/TAKDataPackageGenerator.swift | 261 ++++++++ .../Helpers/TAK/TAKMeshtasticBridge.swift | 516 +++++++++++++++ Meshtastic/Helpers/TAK/TAKServerManager.swift | 427 ++++++++++++ Meshtastic/Meshtastic.entitlements | 2 + Meshtastic/MeshtasticAppDelegate.swift | 4 + .../Resources/Certificates/backup/ca.pem | 20 + .../Resources/Certificates/backup/server.p12 | Bin 0 -> 3459 bytes Meshtastic/Resources/Certificates/ca.pem | 23 + Meshtastic/Resources/Certificates/client.p12 | Bin 0 -> 3827 bytes Meshtastic/Resources/Certificates/server.p12 | Bin 0 -> 3859 bytes Meshtastic/Router/NavigationState.swift | 1 + Meshtastic/Views/Settings/Settings.swift | 15 + .../Views/Settings/TAKServerConfig.swift | 390 +++++++++++ itak-example-data-package/iphone.p12 | Bin 0 -> 3009 bytes itak-example-data-package/manifest.xml | 12 + itak-example-data-package/server.p12 | Bin 0 -> 3009 bytes itak-example-data-package/taky-server.pref | 16 + 32 files changed, 5180 insertions(+), 28 deletions(-) create mode 100644 Meshtastic/Accessory/Accessory Manager/AccessoryManager+TAK.swift delete mode 100644 Meshtastic/Helpers/Logger.swift create mode 100644 Meshtastic/Helpers/TAK/CoTMessage.swift create mode 100644 Meshtastic/Helpers/TAK/CoTXMLParser.swift create mode 100644 Meshtastic/Helpers/TAK/EXICodec.swift create mode 100644 Meshtastic/Helpers/TAK/FountainCodec.swift create mode 100644 Meshtastic/Helpers/TAK/GenericCoTHandler.swift create mode 100644 Meshtastic/Helpers/TAK/TAKCertificateManager.swift create mode 100644 Meshtastic/Helpers/TAK/TAKConnection.swift create mode 100644 Meshtastic/Helpers/TAK/TAKDataPackageGenerator.swift create mode 100644 Meshtastic/Helpers/TAK/TAKMeshtasticBridge.swift create mode 100644 Meshtastic/Helpers/TAK/TAKServerManager.swift create mode 100644 Meshtastic/Resources/Certificates/backup/ca.pem create mode 100644 Meshtastic/Resources/Certificates/backup/server.p12 create mode 100644 Meshtastic/Resources/Certificates/ca.pem create mode 100644 Meshtastic/Resources/Certificates/client.p12 create mode 100644 Meshtastic/Resources/Certificates/server.p12 create mode 100644 Meshtastic/Views/Settings/TAKServerConfig.swift create mode 100644 itak-example-data-package/iphone.p12 create mode 100644 itak-example-data-package/manifest.xml create mode 100644 itak-example-data-package/server.p12 create mode 100644 itak-example-data-package/taky-server.pref diff --git a/Localizable.xcstrings b/Localizable.xcstrings index 33be032d..ee9b52c4 100644 --- a/Localizable.xcstrings +++ b/Localizable.xcstrings @@ -1900,6 +1900,9 @@ } } } + }, + "8089" : { + }, "A channel index of 0 indicates the primary channel where broadcast packets are sent from. Location data is broadcast from the first channel where it is enabled with firmware 2.7 forward." : { "localizations" : { @@ -1910,6 +1913,9 @@ } } } + }, + "A default self-signed certificate is included for localhost connections. Import a custom .p12 if needed. Client CA (.pem) validates connecting TAK clients." : { + }, "A green lock means the channel is securely encrypted with either a 128 or 256 bit AES key." : { "localizations" : { @@ -2463,6 +2469,9 @@ } } } + }, + "Add CA" : { + }, "Add Channel" : { "localizations" : { @@ -7671,6 +7680,12 @@ "Client Base should only favorite other nodes you control. Improper use will hurt your local mesh." : { "comment" : "A message displayed in a confirmation dialog when trying to favorite a node as a CLIENT_BASE.", "isCommentAutoGenerated" : true + }, + "Client CA Certificate" : { + + }, + "Client Configuration" : { + }, "Client Hidden" : { "localizations" : { @@ -8128,6 +8143,9 @@ } } } + }, + "Configuration" : { + }, "Configuration for: %@" : { "localizations" : { @@ -9710,6 +9728,9 @@ } } } + }, + "Delete All" : { + }, "Delete all config, keys and BLE bonds? " : { "localizations" : { @@ -12201,6 +12222,9 @@ } } } + }, + "Download TAK Server Data Package" : { + }, "Drag & Drop Firmware Update" : { "localizations" : { @@ -12714,6 +12738,9 @@ } } } + }, + "Enable TAK Server" : { + }, "Enable this device as a Store and Forward server. Requires an ESP32 device with PSRAM." : { "localizations" : { @@ -13227,6 +13254,12 @@ } } } + }, + "Enter P12 Password" : { + + }, + "Enter the password for the PKCS#12 file" : { + }, "environment" : { "localizations" : { @@ -15937,6 +15970,9 @@ } } } + }, + "Generate a data package (.zip) to configure ITAK or other TAK clients to connect to this server." : { + }, "Generate a new private key to replace the one currently in use. The public key will automatically be regenerated from your private key." : { "localizations" : { @@ -18225,6 +18261,18 @@ } } } + }, + "Import" : { + + }, + "Import .pem" : { + + }, + "Import Custom .p12" : { + + }, + "Import Error" : { + }, "Import Route" : { "localizations" : { @@ -22087,6 +22135,9 @@ } } } + }, + "mTLS" : { + }, "Multiplier" : { "localizations" : { @@ -26312,6 +26363,9 @@ } } } + }, + "Port" : { + }, "Position" : { "localizations" : { @@ -28852,6 +28906,9 @@ } } } + }, + "Reload Bundled Certificates" : { + }, "Remote administration for: %@" : { "localizations" : { @@ -29386,6 +29443,9 @@ } } } + }, + "Reset to Default" : { + }, "Restart" : { "localizations" : { @@ -29420,6 +29480,9 @@ } } } + }, + "Restart Server" : { + }, "Restart to the node you are connected to" : { "localizations" : { @@ -31290,6 +31353,9 @@ } } } + }, + "Secure mTLS connection on port 8089. Both server and client certificates are required." : { + }, "Security" : { "localizations" : { @@ -33101,6 +33167,9 @@ } } } + }, + "Server Certificate" : { + }, "Server Option" : { "localizations" : { @@ -33129,6 +33198,9 @@ } } } + }, + "Server Status" : { + }, "Set" : { "localizations" : { @@ -35045,6 +35117,9 @@ } } } + }, + "Status" : { + }, "Stay Connected Anywhere" : { "localizations" : { @@ -35457,6 +35532,9 @@ } } } + }, + "TAK Server" : { + }, "TAK Tracker" : { "localizations" : { @@ -37757,6 +37835,9 @@ } } } + }, + "TLS Certificates" : { + }, "TLS Enabled" : { "localizations" : { @@ -42321,4 +42402,4 @@ } }, "version" : "1.1" -} +} \ No newline at end of file diff --git a/Meshtastic.xcodeproj/project.pbxproj b/Meshtastic.xcodeproj/project.pbxproj index b87ce5f2..818b65aa 100644 --- a/Meshtastic.xcodeproj/project.pbxproj +++ b/Meshtastic.xcodeproj/project.pbxproj @@ -82,18 +82,29 @@ 25F5D5C02C3F6DA6008036E3 /* Router.swift in Sources */ = {isa = PBXBuildFile; fileRef = 25F5D5BF2C3F6DA6008036E3 /* Router.swift */; }; 25F5D5C22C3F6E4B008036E3 /* AppState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 25F5D5C12C3F6E4B008036E3 /* AppState.swift */; }; 25F5D5D12C4375DF008036E3 /* RouterTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 25F5D5D02C4375DF008036E3 /* RouterTests.swift */; }; + 2849A5E4CE9FDC1DB33DFA34 /* TAKConnection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 01028778B8BFD81F7A039593 /* TAKConnection.swift */; }; + 300424F80C4A445A0FBAE82D /* TAKMeshtasticBridge.swift in Sources */ = {isa = PBXBuildFile; fileRef = 87D006C85B250291D5925F30 /* TAKMeshtasticBridge.swift */; }; 3D3417B42E2730EC006A988B /* GeoJSONOverlayManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3D3417B32E2730EC006A988B /* GeoJSONOverlayManager.swift */; }; 3D3417C82E29D38A006A988B /* GeoJSONOverlayConfig.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3D3417C72E29D38A006A988B /* GeoJSONOverlayConfig.swift */; }; 3D3417D22E2DC260006A988B /* MapDataManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3D3417D12E2DC260006A988B /* MapDataManager.swift */; }; 3D3417D42E2DC293006A988B /* MapDataFiles.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3D3417D32E2DC293006A988B /* MapDataFiles.swift */; }; + 655AF7816E76D5F310DF87A6 /* FountainCodec.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3F203877F307073096C89179 /* FountainCodec.swift */; }; 6D825E622C34786C008DBEE4 /* CommonRegex.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6D825E612C34786C008DBEE4 /* CommonRegex.swift */; }; 6DA39D8E2A92DC52007E311C /* MeshtasticAppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6DA39D8D2A92DC52007E311C /* MeshtasticAppDelegate.swift */; }; 6DEDA55A2A957B8E00321D2E /* DetectionSensorLog.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6DEDA5592A957B8E00321D2E /* DetectionSensorLog.swift */; }; 6DEDA55C2A9592F900321D2E /* MessageEntityExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6DEDA55B2A9592F900321D2E /* MessageEntityExtension.swift */; }; + 7CCBCA0251DAB58FD9D63D06 /* GenericCoTHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7F1B62B5CB54395476C3A924 /* GenericCoTHandler.swift */; }; + 8398407DBA32EE7CFC16A385 /* TAKDataPackageGenerator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9155703C39B55FC9DDF3E4C1 /* TAKDataPackageGenerator.swift */; }; + 8A8F2D8A3769D24BAB88B4A1 /* CoTMessage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4AA216CF50721EE1AE7D7251 /* CoTMessage.swift */; }; 8D3F8A3F2D44BB02009EAAA4 /* PowerMetrics.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8D3F8A3E2D44BB02009EAAA4 /* PowerMetrics.swift */; }; 8D3F8A412D44C2A6009EAAA4 /* PowerMetricsLog.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8D3F8A402D44C2A6009EAAA4 /* PowerMetricsLog.swift */; }; + 8E587743574CE17703E892C6 /* Certificates in Resources */ = {isa = PBXBuildFile; fileRef = 518D504DED9874EBF9D76578 /* Certificates */; }; + 8EED425B7820DA4FEB40C375 /* CoTXMLParser.swift in Sources */ = {isa = PBXBuildFile; fileRef = 748E4806582595DE80D455CD /* CoTXMLParser.swift */; }; + 9604373EEB96801AA89DF48C /* EXICodec.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3D0A8ABAEF1E587683970927 /* EXICodec.swift */; }; + A5339E2F74E83F8FC41EEE33 /* TAKServerConfig.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0618E6D0DF90B74EE32E6C06 /* TAKServerConfig.swift */; }; ABA8E6402E2F2A2300E27791 /* AppIconButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABA8E63F2E2F2A2300E27791 /* AppIconButton.swift */; }; ABB99DEB2E2EA1C500CFBD05 /* AppIconPicker.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABB99DEA2E2EA1C500CFBD05 /* AppIconPicker.swift */; }; + B16C760DB291CFAB5335EADB /* TAKCertificateManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 09936BEBD6D82479B2360FDC /* TAKCertificateManager.swift */; }; B399E8A42B6F486400E4488E /* RetryButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = B399E8A32B6F486400E4488E /* RetryButton.swift */; }; B3E905B12B71F7F300654D07 /* TextMessageField.swift in Sources */ = {isa = PBXBuildFile; fileRef = B3E905B02B71F7F300654D07 /* TextMessageField.swift */; }; BC10380F2DD4334400B00BFA /* AddContactIntent.swift in Sources */ = {isa = PBXBuildFile; fileRef = BC10380E2DD4333C00B00BFA /* AddContactIntent.swift */; }; @@ -296,6 +307,8 @@ DDF924CA26FBB953009FE055 /* ConnectedDevice.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDF924C926FBB953009FE055 /* ConnectedDevice.swift */; }; DDFEB3BB29900C1200EE7472 /* CurrentConditionsCompact.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDFEB3BA29900C1200EE7472 /* CurrentConditionsCompact.swift */; }; DDFFA7472B3A7F3C004730DB /* Bundle.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDFFA7462B3A7F3C004730DB /* Bundle.swift */; }; + E3ED80145D0E873011982556 /* TAKServerManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2B37CCEE8B44A4BA123ED118 /* TAKServerManager.swift */; }; + FE508F9AF5AD5DA20AA64DBF /* AccessoryManager+TAK.swift in Sources */ = {isa = PBXBuildFile; fileRef = 82232A3CF2DD284ED5B9B8ED /* AccessoryManager+TAK.swift */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -330,6 +343,9 @@ /* End PBXCopyFilesBuildPhase section */ /* Begin PBXFileReference section */ + 01028778B8BFD81F7A039593 /* TAKConnection.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = TAKConnection.swift; sourceTree = ""; }; + 0618E6D0DF90B74EE32E6C06 /* TAKServerConfig.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = TAKServerConfig.swift; sourceTree = ""; }; + 09936BEBD6D82479B2360FDC /* TAKCertificateManager.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = TAKCertificateManager.swift; sourceTree = ""; }; 108FFECA2DD3F43C00BFAA81 /* ShareContactQRDialog.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShareContactQRDialog.swift; sourceTree = ""; }; 108FFECC2DD4005600BFAA81 /* NodeInfoEntityToNodeInfo.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NodeInfoEntityToNodeInfo.swift; sourceTree = ""; }; 230BC3962E31071E0046BF2A /* AccessoryManager+Discovery.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "AccessoryManager+Discovery.swift"; sourceTree = ""; }; @@ -393,17 +409,27 @@ 25F5D5C12C3F6E4B008036E3 /* AppState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppState.swift; sourceTree = ""; }; 25F5D5C72C4375A8008036E3 /* MeshtasticTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = MeshtasticTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 25F5D5D02C4375DF008036E3 /* RouterTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RouterTests.swift; sourceTree = ""; }; + 2B37CCEE8B44A4BA123ED118 /* TAKServerManager.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = TAKServerManager.swift; sourceTree = ""; }; + 3D0A8ABAEF1E587683970927 /* EXICodec.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = EXICodec.swift; sourceTree = ""; }; 3D3417B32E2730EC006A988B /* GeoJSONOverlayManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GeoJSONOverlayManager.swift; sourceTree = ""; }; 3D3417C72E29D38A006A988B /* GeoJSONOverlayConfig.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GeoJSONOverlayConfig.swift; sourceTree = ""; }; 3D3417D12E2DC260006A988B /* MapDataManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MapDataManager.swift; sourceTree = ""; }; 3D3417D32E2DC293006A988B /* MapDataFiles.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MapDataFiles.swift; sourceTree = ""; }; + 3F203877F307073096C89179 /* FountainCodec.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = FountainCodec.swift; sourceTree = ""; }; + 4AA216CF50721EE1AE7D7251 /* CoTMessage.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = CoTMessage.swift; sourceTree = ""; }; + 518D504DED9874EBF9D76578 /* Certificates */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = folder; name = Certificates; path = Certificates; sourceTree = ""; }; 6D825E612C34786C008DBEE4 /* CommonRegex.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CommonRegex.swift; sourceTree = ""; }; 6DA39D8D2A92DC52007E311C /* MeshtasticAppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MeshtasticAppDelegate.swift; sourceTree = ""; }; 6DEDA5592A957B8E00321D2E /* DetectionSensorLog.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DetectionSensorLog.swift; sourceTree = ""; }; 6DEDA55B2A9592F900321D2E /* MessageEntityExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageEntityExtension.swift; sourceTree = ""; }; + 748E4806582595DE80D455CD /* CoTXMLParser.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = CoTXMLParser.swift; sourceTree = ""; }; + 7F1B62B5CB54395476C3A924 /* GenericCoTHandler.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = GenericCoTHandler.swift; sourceTree = ""; }; + 82232A3CF2DD284ED5B9B8ED /* AccessoryManager+TAK.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = "AccessoryManager+TAK.swift"; sourceTree = ""; }; + 87D006C85B250291D5925F30 /* TAKMeshtasticBridge.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = TAKMeshtasticBridge.swift; sourceTree = ""; }; 8D3F8A3D2D44B137009EAAA4 /* MeshtasticDataModelV 49.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = "MeshtasticDataModelV 49.xcdatamodel"; sourceTree = ""; }; 8D3F8A3E2D44BB02009EAAA4 /* PowerMetrics.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PowerMetrics.swift; sourceTree = ""; }; 8D3F8A402D44C2A6009EAAA4 /* PowerMetricsLog.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PowerMetricsLog.swift; sourceTree = ""; }; + 9155703C39B55FC9DDF3E4C1 /* TAKDataPackageGenerator.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = TAKDataPackageGenerator.swift; sourceTree = ""; }; ABA8E63F2E2F2A2300E27791 /* AppIconButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppIconButton.swift; sourceTree = ""; }; ABB99DEA2E2EA1C500CFBD05 /* AppIconPicker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppIconPicker.swift; sourceTree = ""; }; B399E8A32B6F486400E4488E /* RetryButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RetryButton.swift; sourceTree = ""; }; @@ -670,7 +696,17 @@ /* End PBXFileReference section */ /* Begin PBXFileSystemSynchronizedRootGroup section */ - DD4C11E02E8099C3003F2F2E /* PreferenceKeys */ = {isa = PBXFileSystemSynchronizedRootGroup; explicitFileTypes = {}; explicitFolders = (); path = PreferenceKeys; sourceTree = ""; }; + DD4C11E02E8099C3003F2F2E /* PreferenceKeys */ = { + isa = PBXFileSystemSynchronizedRootGroup; + exceptions = ( + ); + explicitFileTypes = { + }; + explicitFolders = ( + ); + path = PreferenceKeys; + sourceTree = ""; + }; /* End PBXFileSystemSynchronizedRootGroup section */ /* Begin PBXFrameworksBuildPhase section */ @@ -788,6 +824,7 @@ 23AD54682E2A6EAA0046E9AB /* AccessoryManager+FromRadio.swift */, 23AD546A2E2AA5A80046E9AB /* AccessoryManager+ToRadio.swift */, 23AD546C2E2AE9630046E9AB /* AccessoryManager+MQTT.swift */, + 82232A3CF2DD284ED5B9B8ED /* AccessoryManager+TAK.swift */, ); path = "Accessory Manager"; sourceTree = ""; @@ -892,6 +929,24 @@ path = AppIntents; sourceTree = ""; }; + C37572859BC745C4284A9B42 /* TAK */ = { + isa = PBXGroup; + children = ( + 4AA216CF50721EE1AE7D7251 /* CoTMessage.swift */, + 748E4806582595DE80D455CD /* CoTXMLParser.swift */, + 09936BEBD6D82479B2360FDC /* TAKCertificateManager.swift */, + 01028778B8BFD81F7A039593 /* TAKConnection.swift */, + 87D006C85B250291D5925F30 /* TAKMeshtasticBridge.swift */, + 2B37CCEE8B44A4BA123ED118 /* TAKServerManager.swift */, + 9155703C39B55FC9DDF3E4C1 /* TAKDataPackageGenerator.swift */, + 3F203877F307073096C89179 /* FountainCodec.swift */, + 3D0A8ABAEF1E587683970927 /* EXICodec.swift */, + 7F1B62B5CB54395476C3A924 /* GenericCoTHandler.swift */, + ); + name = TAK; + path = TAK; + sourceTree = ""; + }; D9C9839E2B79D0C600BDBE6A /* TextMessageField */ = { isa = PBXGroup; children = ( @@ -978,6 +1033,7 @@ DD3CC6B428E33FD100FA9159 /* ShareChannels.swift */, DDCE4E2B2869F92900BE9F8F /* UserConfig.swift */, ABA8E63F2E2F2A2300E27791 /* AppIconButton.swift */, + 0618E6D0DF90B74EE32E6C06 /* TAKServerConfig.swift */, ); path = Settings; sourceTree = ""; @@ -1231,6 +1287,7 @@ DDB75A192A05EB67006ED576 /* alpha.png */, DDC2E15B26CE248F0042C5E4 /* Assets.xcassets */, DD0E21002B8A6BC500F2D100 /* DeviceHardware.json */, + 518D504DED9874EBF9D76578 /* Certificates */, ); path = Resources; sourceTree = ""; @@ -1295,6 +1352,7 @@ DD3619142B1EF9F900C41C8C /* LocationsHandler.swift */, 6D825E612C34786C008DBEE4 /* CommonRegex.swift */, 3D3417B32E2730EC006A988B /* GeoJSONOverlayManager.swift */, + C37572859BC745C4284A9B42 /* TAK */, ); path = Helpers; sourceTree = ""; @@ -1564,6 +1622,7 @@ DDC2E15C26CE248F0042C5E4 /* Assets.xcassets in Resources */, DD0E21012B8A6F1300F2D100 /* DeviceHardware.json in Resources */, DDDBC87B2BC62E4E001E8DF7 /* Settings.bundle in Resources */, + 8E587743574CE17703E892C6 /* Certificates in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -1869,6 +1928,18 @@ BC6B45FF2CB2F98900723CEB /* SaveChannelSettingsIntent.swift in Sources */, D93068D72B8146690066FBC8 /* MessageText.swift in Sources */, DDC2E15826CE248E0042C5E4 /* MeshtasticApp.swift in Sources */, + 8A8F2D8A3769D24BAB88B4A1 /* CoTMessage.swift in Sources */, + 8EED425B7820DA4FEB40C375 /* CoTXMLParser.swift in Sources */, + B16C760DB291CFAB5335EADB /* TAKCertificateManager.swift in Sources */, + 2849A5E4CE9FDC1DB33DFA34 /* TAKConnection.swift in Sources */, + 300424F80C4A445A0FBAE82D /* TAKMeshtasticBridge.swift in Sources */, + E3ED80145D0E873011982556 /* TAKServerManager.swift in Sources */, + FE508F9AF5AD5DA20AA64DBF /* AccessoryManager+TAK.swift in Sources */, + A5339E2F74E83F8FC41EEE33 /* TAKServerConfig.swift in Sources */, + 8398407DBA32EE7CFC16A385 /* TAKDataPackageGenerator.swift in Sources */, + 655AF7816E76D5F310DF87A6 /* FountainCodec.swift in Sources */, + 9604373EEB96801AA89DF48C /* EXICodec.swift in Sources */, + 7CCBCA0251DAB58FD9D63D06 /* GenericCoTHandler.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; diff --git a/Meshtastic.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/Meshtastic.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index 3b339cec..cb5d36cf 100644 --- a/Meshtastic.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/Meshtastic.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -1,5 +1,5 @@ { - "originHash" : "2569905853aec088d5bac6b540eac77f78963f88b406e8dd95a88c40623cc8b4", + "originHash" : "7d747a138ea225de00b815c2d9ed46c704c081d98cc8d1018c8d11cb91f39bc4", "pins" : [ { "identity" : "cocoamqtt", @@ -15,8 +15,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/DataDog/dd-sdk-ios.git", "state" : { - "revision" : "d0a42d8067665cb6ee86af51251ccc071f62bd54", - "version" : "2.29.0" + "revision" : "2cddcb47c021365c5a6ebc377cb379aa979c450e", + "version" : "3.4.0" } }, { @@ -60,8 +60,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/apple/swift-protobuf.git", "state" : { - "revision" : "102a647b573f60f73afdce5613a51d71349fe507", - "version" : "1.30.0" + "revision" : "c169a5744230951031770e27e475ff6eefe51f9d", + "version" : "1.33.3" } } ], diff --git a/Meshtastic/Accessory/Accessory Manager/AccessoryManager+FromRadio.swift b/Meshtastic/Accessory/Accessory Manager/AccessoryManager+FromRadio.swift index 5bcead9b..d8e70f0e 100644 --- a/Meshtastic/Accessory/Accessory Manager/AccessoryManager+FromRadio.swift +++ b/Meshtastic/Accessory/Accessory Manager/AccessoryManager+FromRadio.swift @@ -93,6 +93,8 @@ extension AccessoryManager { } tryClearExistingChannels() + // Initialize TAK bridge for TAK integration + initializeTAKBridge() } func handleNodeInfo(_ nodeInfo: NodeInfo) { diff --git a/Meshtastic/Accessory/Accessory Manager/AccessoryManager+TAK.swift b/Meshtastic/Accessory/Accessory Manager/AccessoryManager+TAK.swift new file mode 100644 index 00000000..d6c96783 --- /dev/null +++ b/Meshtastic/Accessory/Accessory Manager/AccessoryManager+TAK.swift @@ -0,0 +1,209 @@ +// +// AccessoryManager+TAK.swift +// Meshtastic +// +// Created by niccellular 12/26/25 +// + +import Foundation +import MeshtasticProtobufs +import OSLog + +extension AccessoryManager { + + // MARK: - TAK Server Initialization + + /// Initialize the TAK bridge when connected to a Meshtastic device + func initializeTAKBridge() { + let takServer = TAKServerManager.shared + + // Create the bridge + let bridge = TAKMeshtasticBridge( + accessoryManager: self, + takServerManager: takServer + ) + bridge.context = self.context + + // Assign bridge to server + takServer.bridge = bridge + + Logger.tak.info("TAK bridge initialized") + + // Start server if enabled + if takServer.enabled && !takServer.isRunning { + Task { + do { + try await takServer.start() + Logger.tak.info("TAK Server auto-started on connection") + } catch { + Logger.tak.error("Failed to auto-start TAK Server: \(error.localizedDescription)") + } + } + } + } + + /// Clean up TAK bridge when disconnecting + func cleanupTAKBridge() { + // Note: We don't stop the server here - it can continue running + // even without a Meshtastic connection (for TAK connectivity) + Logger.tak.info("TAK bridge cleanup") + } + + // MARK: - Send TAK Packet to Mesh + + /// Send a TAK packet to the Meshtastic mesh network + /// - Parameters: + /// - takPacket: The TAKPacket protobuf to send + /// - channel: Channel to send on (0 = default/primary) + func sendTAKPacket(_ takPacket: TAKPacket, channel: UInt32 = 0) async throws { + Logger.tak.debug("=== Sending TAKPacket to Mesh ===") + + guard let activeConnection else { + Logger.tak.error("Not connected to Meshtastic device") + throw AccessoryError.connectionFailed("Not connected to Meshtastic device") + } + + guard let deviceNum = activeConnection.device.num else { + Logger.tak.error("No device number available") + throw AccessoryError.connectionFailed("No device number available") + } + + Logger.tak.debug("Device num: \(deviceNum)") + + // Log TAKPacket details before serialization + Logger.tak.debug("TAKPacket to send:") + Logger.tak.debug(" hasContact: \(takPacket.hasContact)") + if takPacket.hasContact { + Logger.tak.debug(" callsign: \(takPacket.contact.callsign)") + Logger.tak.debug(" deviceCallsign: \(takPacket.contact.deviceCallsign)") + } + Logger.tak.debug(" hasGroup: \(takPacket.hasGroup)") + if takPacket.hasGroup { + Logger.tak.debug(" team: \(takPacket.group.team.rawValue)") + Logger.tak.debug(" role: \(takPacket.group.role.rawValue)") + } + Logger.tak.debug(" hasStatus: \(takPacket.hasStatus)") + if takPacket.hasStatus { + Logger.tak.debug(" battery: \(takPacket.status.battery)") + } + Logger.tak.debug(" payloadVariant: \(String(describing: takPacket.payloadVariant))") + + // Serialize the TAK packet + let serialized: Data + do { + serialized = try takPacket.serializedData() + Logger.tak.debug("Serialized TAKPacket: \(serialized.count) bytes") + Logger.tak.debug("Serialized hex: \(serialized.map { String(format: "%02x", $0) }.joined(separator: " "))") + } catch { + Logger.tak.error("Failed to serialize TAKPacket: \(error.localizedDescription)") + throw AccessoryError.ioFailed("Failed to serialize TAKPacket") + } + + // Build the mesh packet + var dataMessage = DataMessage() + dataMessage.portnum = .atakPlugin // Port 72 + dataMessage.payload = serialized + + var meshPacket = MeshPacket() + meshPacket.to = 0xFFFFFFFF // Broadcast + meshPacket.from = UInt32(deviceNum) + meshPacket.channel = channel + meshPacket.id = UInt32.random(in: UInt32(UInt8.max)..= 2 && payload[0] == 0x08 && payload[1] == 0x01 { + Logger.tak.debug("Ignoring compressed TAKPacket (duplicate of uncompressed)") + return + } + + // Parse uncompressed TAKPacket protobuf + let takPacket: TAKPacket + do { + takPacket = try TAKPacket(serializedBytes: payload) + } catch { + Logger.tak.warning("Failed to parse TAKPacket from mesh packet: \(error.localizedDescription)") + Logger.tak.debug("Parse error details: \(error)") + Logger.tak.debug("Raw payload hex: \(payload.map { String(format: "%02x", $0) }.joined(separator: " "))") + return + } + + Logger.tak.info("Received TAKPacket from mesh node \(packet.from)") + Logger.tak.debug(" hasContact: \(takPacket.hasContact), hasGroup: \(takPacket.hasGroup), hasStatus: \(takPacket.hasStatus)") + Logger.tak.debug(" payloadVariant: \(String(describing: takPacket.payloadVariant))") + + // Forward to TAK clients via bridge + Task { + await TAKServerManager.shared.bridge?.broadcastToTAKClients(takPacket, from: packet.from) + } + } + + // MARK: - Handle ATAK Forwarder Packet (Port 257) + + /// Handle incoming ATAK_FORWARDER packet for generic CoT events + /// These are EXI-compressed CoT XML, possibly fountain-coded for large messages + func handleATAKForwarderPacket(_ packet: MeshPacket) { + guard case let .decoded(data) = packet.payloadVariant else { + Logger.tak.warning("Received ATAK_FORWARDER packet without decoded payload") + return + } + + Logger.tak.debug("Received ATAK_FORWARDER packet: \(data.payload.count) bytes from node \(packet.from)") + + // Process through GenericCoTHandler on main actor + let packetCopy = packet + let accessoryManagerRef = self + Task { @MainActor in + let handler = GenericCoTHandler.shared + handler.accessoryManager = accessoryManagerRef + + if let cotMessage = handler.handleIncomingForwarderPacket(packetCopy) { + // Forward to TAK clients via the server manager + await TAKServerManager.shared.broadcast(cotMessage) + Logger.tak.info("Forwarded generic CoT to TAK clients: \(cotMessage.type)") + } + } + } +} diff --git a/Meshtastic/Accessory/Accessory Manager/AccessoryManager.swift b/Meshtastic/Accessory/Accessory Manager/AccessoryManager.swift index 07513866..13490459 100644 --- a/Meshtastic/Accessory/Accessory Manager/AccessoryManager.swift +++ b/Meshtastic/Accessory/Accessory Manager/AccessoryManager.swift @@ -575,7 +575,7 @@ class AccessoryManager: ObservableObject, MqttClientProxyManagerDelegate { case .privateApp: Logger.mesh.info("🕸️ MESH PACKET received for Private App UNHANDLED UNHANDLED") case .atakForwarder: - Logger.mesh.info("🕸️ MESH PACKET received for ATAK Forwarder App UNHANDLED UNHANDLED") + handleATAKForwarderPacket(packet) case .simulatorApp: Logger.mesh.info("🕸️ MESH PACKET received for Simulator App UNHANDLED UNHANDLED") case .storeForwardPlusplusApp: @@ -597,7 +597,7 @@ class AccessoryManager: ObservableObject, MqttClientProxyManagerDelegate { case .max: Logger.services.info("MAX PORT NUM OF 511") case .atakPlugin: - Logger.mesh.info("🕸️ MESH PACKET received for ATAK Plugin App UNHANDLED \((try? decodedInfo.packet.jsonString()) ?? "JSON Decode Failure", privacy: .public)") + handleATAKPluginPacket(packet) case .powerstressApp: Logger.mesh.info("🕸️ MESH PACKET received for Power Stress App UNHANDLED \((try? decodedInfo.packet.jsonString()) ?? "JSON Decode Failure", privacy: .public)") case .reticulumTunnelApp: diff --git a/Meshtastic/Extensions/Logger.swift b/Meshtastic/Extensions/Logger.swift index a67f32d1..fb04f66f 100644 --- a/Meshtastic/Extensions/Logger.swift +++ b/Meshtastic/Extensions/Logger.swift @@ -36,6 +36,9 @@ extension Logger { /// All logs related to the transport layer static let transport = Logger(subsystem: subsystem, category: "🚚 Transport") + /// All logs related to TAK server and CoT messages + static let tak = Logger(subsystem: subsystem, category: "🎯 TAK") + /// Fetch from the logstore static public func fetch(predicateFormat: String) async throws -> [OSLogEntryLog] { diff --git a/Meshtastic/Helpers/Logger.swift b/Meshtastic/Helpers/Logger.swift deleted file mode 100644 index 35ee7337..00000000 --- a/Meshtastic/Helpers/Logger.swift +++ /dev/null @@ -1,19 +0,0 @@ -import OSLog - -extension Logger { - - /// The logger's subsystem. - private static var subsystem = Bundle.main.bundleIdentifier! - - /// All logs related to data such as decoding error, parsing issues, etc. - public static let data = Logger(subsystem: subsystem, category: "🗄️ Data") - - /// All logs related to the mesh - public static let mesh = Logger(subsystem: subsystem, category: "🕸️ Mesh") - - /// All logs related to services such as network calls, location, etc. - public static let services = Logger(subsystem: subsystem, category: "🍏 Services") - - /// All logs related to tracking and analytics. - public static let statistics = Logger(subsystem: subsystem, category: "📈 Stats") -} diff --git a/Meshtastic/Helpers/TAK/CoTMessage.swift b/Meshtastic/Helpers/TAK/CoTMessage.swift new file mode 100644 index 00000000..1d8419b8 --- /dev/null +++ b/Meshtastic/Helpers/TAK/CoTMessage.swift @@ -0,0 +1,527 @@ +// +// CoTMessage.swift +// Meshtastic +// +// Created by niccellular 12/26/25 +// + +import Foundation +import MeshtasticProtobufs +import CoreLocation + +/// Cursor on Target (CoT) message representation +/// Handles both parsing incoming CoT XML and generating outgoing CoT XML +struct CoTMessage: Identifiable, Sendable { + let id = UUID() + + // MARK: - Core CoT Event Attributes + + /// Unique identifier for this event + var uid: String + + /// CoT type (e.g., "a-f-G-U-C" for friendly ground unit, "b-t-f" for chat) + var type: String + + /// Event generation time + var time: Date + + /// Start of event validity + var start: Date + + /// When this event becomes stale + var stale: Date + + /// How the event was generated (e.g., "m-g" for machine GPS, "h-g-i-g-o" for human generated) + var how: String + + // MARK: - Point Element (Location) + + /// Latitude in degrees + var latitude: Double + + /// Longitude in degrees + var longitude: Double + + /// Height above ellipsoid in meters + var hae: Double + + /// Circular error in meters + var ce: Double + + /// Linear error in meters + var le: Double + + // MARK: - Detail Elements + + /// Contact information (callsign, endpoint) + var contact: CoTContact? + + /// Group/team assignment + var group: CoTGroup? + + /// Device status (battery) + var status: CoTStatus? + + /// Movement track (speed, course) + var track: CoTTrack? + + /// Chat message details + var chat: CoTChat? + + /// Remarks/comments text + var remarks: String? + + /// Raw detail XML content for elements we don't explicitly parse + /// Used to preserve generic CoT elements (colors, shapes, labels, etc.) + var rawDetailXML: String? + + // MARK: - Initialization + + init( + uid: String, + type: String, + time: Date = Date(), + start: Date = Date(), + stale: Date = Date().addingTimeInterval(600), + how: String = "m-g", + latitude: Double = 0, + longitude: Double = 0, + hae: Double = 9999999.0, + ce: Double = 9999999.0, + le: Double = 9999999.0, + contact: CoTContact? = nil, + group: CoTGroup? = nil, + status: CoTStatus? = nil, + track: CoTTrack? = nil, + chat: CoTChat? = nil, + remarks: String? = nil, + rawDetailXML: String? = nil + ) { + self.uid = uid + self.type = type + self.time = time + self.start = start + self.stale = stale + self.how = how + self.latitude = latitude + self.longitude = longitude + self.hae = hae + self.ce = ce + self.le = le + self.contact = contact + self.group = group + self.status = status + self.track = track + self.chat = chat + self.remarks = remarks + self.rawDetailXML = rawDetailXML + } + + // MARK: - Factory Methods + + /// Create a PLI (Position Location Information) message for a friendly ground unit + static func pli( + uid: String, + callsign: String, + latitude: Double, + longitude: Double, + altitude: Double = 9999999.0, + speed: Double = 0, + course: Double = 0, + team: String = "Cyan", + role: String = "Team Member", + battery: Int = 100, + staleMinutes: Int = 10 + ) -> CoTMessage { + let now = Date() + return CoTMessage( + uid: uid, + type: "a-f-G-U-C", + time: now, + start: now, + stale: now.addingTimeInterval(TimeInterval(staleMinutes * 60)), + how: "m-g", + latitude: latitude, + longitude: longitude, + hae: altitude, + ce: 9999999.0, + le: 9999999.0, + contact: CoTContact(callsign: callsign, endpoint: "0.0.0.0:4242:tcp"), + group: CoTGroup(name: team, role: role), + status: CoTStatus(battery: battery), + track: CoTTrack(speed: speed, course: course) + ) + } + + /// Create a chat message (b-t-f type for outgoing) + static func chat( + senderUid: String, + senderCallsign: String, + message: String, + chatroom: String = "All Chat Rooms" + ) -> CoTMessage { + let now = Date() + let messageId = UUID().uuidString + return CoTMessage( + uid: "GeoChat.\(senderUid).\(chatroom).\(messageId)", + type: "b-t-f", + time: now, + start: now, + stale: now.addingTimeInterval(86400), + how: "h-g-i-g-o", + latitude: 0, + longitude: 0, + hae: 9999999.0, + ce: 9999999.0, + le: 9999999.0, + chat: CoTChat( + message: message, + senderCallsign: senderCallsign, + chatroom: chatroom + ), + remarks: message + ) + } + + // MARK: - Create from Meshtastic TAKPacket + + /// Convert Meshtastic TAKPacket protobuf to CoT message + static func fromTAKPacket(_ takPacket: TAKPacket, deviceUid: String? = nil) -> CoTMessage? { + let currentDate = Date() + let staleDate = currentDate.addingTimeInterval(10 * 60) // 10 minute stale + + // Handle PLI (Position Location Information) + if case .pli(let pli) = takPacket.payloadVariant { + // Validate we have required fields + guard takPacket.hasContact, + pli.latitudeI != 0 || pli.longitudeI != 0 else { + return nil + } + + // Parse device_callsign in case it contains smuggled messageId (shouldn't for PLI, but be safe) + let (actualDeviceCallsign, _) = TAKMeshtasticBridge.parseDeviceCallsign(takPacket.contact.deviceCallsign) + let uid = actualDeviceCallsign.isEmpty + ? (deviceUid ?? UUID().uuidString) + : actualDeviceCallsign + + return CoTMessage( + uid: uid, + type: "a-f-G-U-C", + time: currentDate, + start: currentDate, + stale: staleDate, + how: "m-g", + latitude: Double(pli.latitudeI) * 1e-7, + longitude: Double(pli.longitudeI) * 1e-7, + hae: Double(pli.altitude), + ce: 9999999.0, + le: 9999999.0, + contact: CoTContact( + callsign: takPacket.contact.callsign, + endpoint: "0.0.0.0:4242:tcp" + ), + group: takPacket.hasGroup ? CoTGroup( + name: takPacket.group.team.cotColorName, + role: takPacket.group.role.cotRoleName + ) : CoTGroup(name: "Cyan", role: "Team Member"), + status: takPacket.hasStatus ? CoTStatus( + battery: Int(takPacket.status.battery) + ) : nil, + track: CoTTrack( + speed: Double(pli.speed), + course: Double(pli.course) + ) + ) + } + + // Handle GeoChat + if case .chat(let geoChat) = takPacket.payloadVariant { + // Parse device_callsign which may contain smuggled messageId + // Format: "|" or just "" + let rawDeviceCallsign = takPacket.hasContact ? takPacket.contact.deviceCallsign : "" + let (actualDeviceCallsign, smuggledMessageId) = TAKMeshtasticBridge.parseDeviceCallsign(rawDeviceCallsign) + + let uid = actualDeviceCallsign.isEmpty + ? (deviceUid ?? UUID().uuidString) + : actualDeviceCallsign + + let chatroom = geoChat.hasTo ? geoChat.to : "All Chat Rooms" + // Use smuggled messageId if present, otherwise generate new one + let messageId = smuggledMessageId ?? UUID().uuidString + + return CoTMessage( + uid: "GeoChat.\(uid).\(chatroom).\(messageId)", + type: "b-t-f", + time: currentDate, + start: currentDate, + stale: currentDate.addingTimeInterval(86400), + how: "h-g-i-g-o", + latitude: 0, + longitude: 0, + hae: 9999999.0, + ce: 9999999.0, + le: 9999999.0, + contact: takPacket.hasContact ? CoTContact( + callsign: takPacket.contact.callsign, + endpoint: "0.0.0.0:4242:tcp" + ) : nil, + chat: CoTChat( + message: geoChat.message, + senderCallsign: takPacket.hasContact ? takPacket.contact.callsign : nil, + chatroom: chatroom + ), + remarks: geoChat.message + ) + } + + return nil + } + + // MARK: - XML Generation + + /// Generate CoT XML string for transmission to TAK clients + func toXML() -> String { + let dateFormatter = ISO8601DateFormatter() + dateFormatter.formatOptions = [.withInternetDateTime, .withFractionalSeconds] + + var cot = "" + cot += "" + cot += "", with: ">") + .replacingOccurrences(of: "\"", with: """) + .replacingOccurrences(of: "'", with: "'") + } +} + +// MARK: - Team/Role Extensions for Meshtastic Protobufs + +extension Team { + /// Convert Meshtastic Team enum to CoT color name + var cotColorName: String { + switch self { + case .white: return "White" + case .yellow: return "Yellow" + case .orange: return "Orange" + case .magenta: return "Magenta" + case .red: return "Red" + case .maroon: return "Maroon" + case .purple: return "Purple" + case .darkBlue: return "Dark Blue" + case .blue: return "Blue" + case .cyan: return "Cyan" + case .teal: return "Teal" + case .green: return "Green" + case .darkGreen: return "Dark Green" + case .brown: return "Brown" + case .unspecifedColor: return "Cyan" + case .UNRECOGNIZED: return "Cyan" + } + } + + /// Create Team from CoT color name + static func fromColorName(_ name: String) -> Team { + switch name.lowercased() { + case "white": return .white + case "yellow": return .yellow + case "orange": return .orange + case "magenta": return .magenta + case "red": return .red + case "maroon": return .maroon + case "purple": return .purple + case "dark blue", "darkblue": return .darkBlue + case "blue": return .blue + case "cyan": return .cyan + case "teal": return .teal + case "green": return .green + case "dark green", "darkgreen": return .darkGreen + case "brown": return .brown + default: return .cyan + } + } +} + +extension MemberRole { + /// Convert Meshtastic MemberRole enum to CoT role name + var cotRoleName: String { + switch self { + case .teamMember: return "Team Member" + case .teamLead: return "Team Lead" + case .hq: return "HQ" + case .sniper: return "Sniper" + case .medic: return "Medic" + case .forwardObserver: return "Forward Observer" + case .rto: return "RTO" + case .k9: return "K9" + case .unspecifed: return "Team Member" + case .UNRECOGNIZED: return "Team Member" + } + } + + /// Create MemberRole from CoT role name + static func fromRoleName(_ name: String) -> MemberRole { + switch name.lowercased() { + case "team member": return .teamMember + case "team lead": return .teamLead + case "hq", "headquarters": return .hq + case "sniper": return .sniper + case "medic": return .medic + case "forward observer": return .forwardObserver + case "rto": return .rto + case "k9": return .k9 + default: return .teamMember + } + } +} + +// MARK: - XML Parsing + +extension CoTMessage { + /// Parse a CoT XML string into a CoTMessage + /// - Parameter xml: The CoT XML string + /// - Returns: Parsed CoTMessage, or nil if parsing failed + static func parse(from xml: String) -> CoTMessage? { + guard let data = xml.data(using: .utf8) else { + return nil + } + + // Use the existing CoTXMLParser class + let parser = CoTXMLParser(data: data) + do { + return try parser.parse() + } catch { + return nil + } + } +} diff --git a/Meshtastic/Helpers/TAK/CoTXMLParser.swift b/Meshtastic/Helpers/TAK/CoTXMLParser.swift new file mode 100644 index 00000000..1c189c3e --- /dev/null +++ b/Meshtastic/Helpers/TAK/CoTXMLParser.swift @@ -0,0 +1,343 @@ +// +// CoTXMLParser.swift +// Meshtastic +// +// Created by niccellular 12/26/25 +// + +import Foundation +import OSLog + +/// XML Parser delegate for parsing incoming CoT (Cursor on Target) messages from TAK clients +final class CoTXMLParser: NSObject, XMLParserDelegate { + private let data: Data + private var cotMessage: CoTMessage? + private var parseError: Error? + + // Current parsing state + private var currentElement = "" + private var currentText = "" + + // Temporary attribute storage during parsing + private var eventAttributes: [String: String] = [:] + private var pointAttributes: [String: String] = [:] + private var contactAttributes: [String: String] = [:] + private var groupAttributes: [String: String] = [:] + private var statusAttributes: [String: String] = [:] + private var trackAttributes: [String: String] = [:] + private var chatAttributes: [String: String] = [:] + private var chatgrpAttributes: [String: String] = [:] + private var remarksAttributes: [String: String] = [:] + private var remarksText = "" + private var linkAttributes: [String: String] = [:] + + // Track element hierarchy for nested elements + private var elementStack: [String] = [] + + // Raw detail XML for unrecognized elements (markers, shapes, colors, etc.) + private var rawDetailXML = "" + private var isCapturingRawDetail = false + private var rawDetailDepth = 0 + + // Known detail elements we handle explicitly + private let knownDetailElements: Set = [ + "contact", "__group", "status", "track", "__chat", "chatgrp", + "remarks", "link", "uid", "__serverdestination" + ] + + init(data: Data) { + self.data = data + } + + /// Parse the XML data and return a CoTMessage + func parse() throws -> CoTMessage { + let parser = XMLParser(data: data) + parser.delegate = self + parser.shouldProcessNamespaces = false + parser.shouldReportNamespacePrefixes = false + + guard parser.parse() else { + if let error = parseError { + throw error + } + throw CoTParseError.parseFailed(parser.parserError?.localizedDescription ?? "Unknown error") + } + + guard let message = cotMessage else { + throw CoTParseError.invalidMessage + } + + return message + } + + // MARK: - XMLParserDelegate + + func parser(_ parser: XMLParser, didStartElement elementName: String, + namespaceURI: String?, qualifiedName qName: String?, + attributes attributeDict: [String: String] = [:]) { + elementStack.append(elementName) + currentElement = elementName + currentText = "" + + // Check if we're inside and this is an unrecognized element + let isInsideDetail = elementStack.contains("detail") && elementName != "detail" + + if isCapturingRawDetail { + // Continue capturing nested elements + rawDetailDepth += 1 + rawDetailXML += buildOpeningTag(elementName, attributes: attributeDict) + } else if isInsideDetail && !knownDetailElements.contains(elementName) { + // Start capturing this unrecognized element + isCapturingRawDetail = true + rawDetailDepth = 1 + rawDetailXML += buildOpeningTag(elementName, attributes: attributeDict) + } + + switch elementName { + case "event": + eventAttributes = attributeDict + case "point": + pointAttributes = attributeDict + case "contact": + contactAttributes = attributeDict + case "__group": + groupAttributes = attributeDict + case "status": + statusAttributes = attributeDict + case "track": + trackAttributes = attributeDict + case "__chat": + chatAttributes = attributeDict + case "chatgrp": + chatgrpAttributes = attributeDict + case "remarks": + remarksAttributes = attributeDict + case "link": + linkAttributes = attributeDict + default: + break + } + } + + /// Build an XML opening tag with attributes + private func buildOpeningTag(_ elementName: String, attributes: [String: String]) -> String { + var tag = "<\(elementName)" + for (key, value) in attributes { + tag += " \(key)='\(value.xmlEscaped)'" + } + tag += ">" + return tag + } + + func parser(_ parser: XMLParser, foundCharacters string: String) { + currentText += string + + // Capture text content for raw detail elements + if isCapturingRawDetail { + rawDetailXML += string.xmlEscaped + } + } + + func parser(_ parser: XMLParser, didEndElement elementName: String, + namespaceURI: String?, qualifiedName qName: String?) { + if elementName == "remarks" { + remarksText = currentText.trimmingCharacters(in: .whitespacesAndNewlines) + } + + // Handle raw detail element closing + if isCapturingRawDetail { + rawDetailXML += "" + rawDetailDepth -= 1 + if rawDetailDepth == 0 { + isCapturingRawDetail = false + } + } + + if elementName == "event" { + buildCoTMessage() + } + + elementStack.removeLast() + currentElement = elementStack.last ?? "" + } + + func parser(_ parser: XMLParser, parseErrorOccurred parseError: Error) { + self.parseError = parseError + Logger.tak.error("CoT XML parse error: \(parseError.localizedDescription)") + } + + // MARK: - Build CoTMessage + + private func buildCoTMessage() { + Logger.tak.debug("=== Building CoTMessage from XML ===") + Logger.tak.debug("Event attributes: \(self.eventAttributes)") + Logger.tak.debug("Point attributes: \(self.pointAttributes)") + Logger.tak.debug("Contact attributes: \(self.contactAttributes)") + Logger.tak.debug("Group attributes: \(self.groupAttributes)") + Logger.tak.debug("Status attributes: \(self.statusAttributes)") + Logger.tak.debug("Track attributes: \(self.trackAttributes)") + Logger.tak.debug("Chat attributes: \(self.chatAttributes)") + Logger.tak.debug("Remarks text: \(self.remarksText)") + + // Parse timestamps + let time = parseDate(eventAttributes["time"]) + let start = parseDate(eventAttributes["start"]) + let stale = parseDate(eventAttributes["stale"]) + + // Build contact if present + var contact: CoTContact? + if !contactAttributes.isEmpty { + contact = CoTContact( + callsign: contactAttributes["callsign"] ?? "", + endpoint: contactAttributes["endpoint"], + phone: contactAttributes["phone"] + ) + Logger.tak.debug("Parsed contact: callsign=\(contact?.callsign ?? "nil")") + } + + // Build group if present + var group: CoTGroup? + if !groupAttributes.isEmpty { + group = CoTGroup( + name: groupAttributes["name"] ?? "Cyan", + role: groupAttributes["role"] ?? "Team Member" + ) + Logger.tak.debug("Parsed group: name=\(group?.name ?? "nil"), role=\(group?.role ?? "nil")") + } + + // Build status if present + var status: CoTStatus? + if let batteryStr = statusAttributes["battery"], let battery = Int(batteryStr) { + status = CoTStatus(battery: battery) + Logger.tak.debug("Parsed status: battery=\(battery)") + } + + // Build track if present + var track: CoTTrack? + if !trackAttributes.isEmpty { + let speed = Double(trackAttributes["speed"] ?? "0") ?? 0 + let course = Double(trackAttributes["course"] ?? "0") ?? 0 + track = CoTTrack(speed: speed, course: course) + Logger.tak.debug("Parsed track: speed=\(speed), course=\(course)") + } + + // Build chat if present + var chat: CoTChat? + if !chatAttributes.isEmpty { + chat = CoTChat( + message: remarksText, + senderCallsign: chatAttributes["senderCallsign"], + chatroom: chatAttributes["chatroom"] ?? chatAttributes["id"] ?? "All Chat Rooms" + ) + Logger.tak.debug("Parsed chat: message=\(self.remarksText.prefix(50)), chatroom=\(chat?.chatroom ?? "nil")") + } + + let uid = eventAttributes["uid"] ?? UUID().uuidString + let type = eventAttributes["type"] ?? "a-f-G-U-C" + let latitude = Double(pointAttributes["lat"] ?? "0") ?? 0 + let longitude = Double(pointAttributes["lon"] ?? "0") ?? 0 + let hae = Double(pointAttributes["hae"] ?? "9999999") ?? 9999999 + + Logger.tak.debug("Building CoTMessage: uid=\(uid), type=\(type)") + Logger.tak.debug(" location: lat=\(latitude), lon=\(longitude), hae=\(hae)") + + cotMessage = CoTMessage( + uid: uid, + type: type, + time: time, + start: start, + stale: stale, + how: eventAttributes["how"] ?? "m-g", + latitude: latitude, + longitude: longitude, + hae: hae, + ce: Double(pointAttributes["ce"] ?? "9999999") ?? 9999999, + le: Double(pointAttributes["le"] ?? "9999999") ?? 9999999, + contact: contact, + group: group, + status: status, + track: track, + chat: chat, + remarks: chat == nil && !remarksText.isEmpty ? remarksText : nil, + rawDetailXML: rawDetailXML.isEmpty ? nil : rawDetailXML + ) + + if !rawDetailXML.isEmpty { + Logger.tak.debug("Captured raw detail XML: \(self.rawDetailXML.prefix(200))...") + } + + Logger.tak.debug("=== CoTMessage built successfully ===") + } + + // MARK: - Date Parsing + + private func parseDate(_ string: String?) -> Date { + guard let string else { return Date() } + + // Try ISO8601 with fractional seconds first + let formatterWithFractional = ISO8601DateFormatter() + formatterWithFractional.formatOptions = [.withInternetDateTime, .withFractionalSeconds] + if let date = formatterWithFractional.date(from: string) { + return date + } + + // Try ISO8601 without fractional seconds + let formatterWithoutFractional = ISO8601DateFormatter() + formatterWithoutFractional.formatOptions = [.withInternetDateTime] + if let date = formatterWithoutFractional.date(from: string) { + return date + } + + // Try basic date formatter + let basicFormatter = DateFormatter() + basicFormatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss'Z'" + basicFormatter.timeZone = TimeZone(identifier: "UTC") + if let date = basicFormatter.date(from: string) { + return date + } + + Logger.tak.warning("Failed to parse CoT date: \(string)") + return Date() + } +} + +// MARK: - Parse Error + +enum CoTParseError: LocalizedError { + case parseFailed(String) + case invalidMessage + case emptyData + + var errorDescription: String? { + switch self { + case .parseFailed(let reason): + return "Failed to parse CoT XML: \(reason)" + case .invalidMessage: + return "Invalid CoT message structure" + case .emptyData: + return "Empty data received" + } + } +} + +// MARK: - CoTMessage Parsing Extension + +extension CoTMessage { + /// Parse CoT XML data into a CoTMessage + static func parse(from data: Data) throws -> CoTMessage { + guard !data.isEmpty else { + throw CoTParseError.emptyData + } + + let parser = CoTXMLParser(data: data) + return try parser.parse() + } + + /// Parse CoT XML string into a CoTMessage + static func parse(from xmlString: String) throws -> CoTMessage { + guard let data = xmlString.data(using: .utf8) else { + throw CoTParseError.emptyData + } + return try parse(from: data) + } +} diff --git a/Meshtastic/Helpers/TAK/EXICodec.swift b/Meshtastic/Helpers/TAK/EXICodec.swift new file mode 100644 index 00000000..e1881e08 --- /dev/null +++ b/Meshtastic/Helpers/TAK/EXICodec.swift @@ -0,0 +1,148 @@ +// +// EXICodec.swift +// Meshtastic +// +// Zlib compression for CoT events over mesh network. +// Uses standard zlib format (78 xx header) for Android interoperability. +// +// IMPORTANT: Uses C zlib library directly to produce standard zlib format. +// Apple's Compression framework produces raw deflate which is NOT compatible +// with Android's standard zlib decompressor. +// +// Zlib header bytes: +// - 78 01: No compression +// - 78 9C: Default compression (what we use) +// - 78 DA: Best compression +// + +import Foundation +import zlib +import OSLog + +/// Codec for compressing/decompressing CoT XML using standard zlib +/// Named EXICodec for historical reasons - now uses zlib for Android compatibility +final class EXICodec { + + static let shared = EXICodec() + + private init() {} + + // MARK: - Compression + + /// Compress CoT XML to binary format using zlib + /// - Parameter xml: The CoT XML string + /// - Returns: Compressed data (78 9C header), or nil if compression failed + func compress(_ xml: String) -> Data? { + guard let xmlData = xml.data(using: .utf8) else { + Logger.tak.error("Zlib: Failed to convert XML to UTF-8 data") + return nil + } + + // Use standard zlib compression (produces 78 9C header that Android expects) + guard let compressed = compressZlib(xmlData) else { + Logger.tak.warning("Zlib: Compression failed, using raw data") + return xmlData + } + + let ratio = Double(compressed.count) / Double(xmlData.count) * 100 + Logger.tak.info("Zlib: Compressed \(xmlData.count) → \(compressed.count) bytes (\(String(format: "%.1f", ratio))%)") + + // Log first few bytes to verify format (should start with 78 9C) + if compressed.count >= 2 { + Logger.tak.debug("Zlib: Header: \(String(format: "%02X %02X", compressed[0], compressed[1]))") + } + + return compressed + } + + /// Decompress zlib data to CoT XML + /// - Parameter data: The compressed data (expects 78 xx header) + /// - Returns: Decompressed XML string, or nil if decompression failed + func decompress(_ data: Data) -> String? { + // Log header for debugging + if data.count >= 2 { + Logger.tak.debug("Zlib: Decompressing data with header: \(String(format: "%02X %02X", data[0], data[1]))") + } + + // Try standard zlib decompression (78 xx header) + if let decompressed = decompressZlib(data) { + if let xml = String(data: decompressed, encoding: .utf8) { + Logger.tak.debug("Zlib: Decompressed \(data.count) → \(decompressed.count) bytes") + return xml + } + } + + // Fallback: try interpreting as raw UTF-8 (uncompressed) + if let xml = String(data: data, encoding: .utf8) { + Logger.tak.debug("Zlib: Data was uncompressed UTF-8 (\(data.count) bytes)") + return xml + } + + Logger.tak.error("Zlib: Failed to decompress data (\(data.count) bytes)") + return nil + } + + // MARK: - Zlib Implementation + + /// Compress data using standard zlib format (78 9C header) + /// Uses C zlib library directly for Android compatibility + private func compressZlib(_ data: Data) -> Data? { + // Calculate maximum compressed size + var compressedLength = compressBound(uLong(data.count)) + var compressed = Data(count: Int(compressedLength)) + + let result = compressed.withUnsafeMutableBytes { destPtr in + data.withUnsafeBytes { srcPtr in + compress2( + destPtr.bindMemory(to: Bytef.self).baseAddress!, + &compressedLength, + srcPtr.bindMemory(to: Bytef.self).baseAddress!, + uLong(data.count), + Z_DEFAULT_COMPRESSION + ) + } + } + + guard result == Z_OK else { + Logger.tak.error("Zlib: compress2 failed with code \(result)") + return nil + } + + return compressed.prefix(Int(compressedLength)) + } + + /// Decompress standard zlib data (78 xx header) + private func decompressZlib(_ data: Data) -> Data? { + // Estimate uncompressed size (start with 10x, will retry if needed) + var uncompressedLength = uLong(data.count * 10) + var maxAttempts = 3 + + while maxAttempts > 0 { + var uncompressed = Data(count: Int(uncompressedLength)) + + let result = uncompressed.withUnsafeMutableBytes { destPtr in + data.withUnsafeBytes { srcPtr in + uncompress( + destPtr.bindMemory(to: Bytef.self).baseAddress!, + &uncompressedLength, + srcPtr.bindMemory(to: Bytef.self).baseAddress!, + uLong(data.count) + ) + } + } + + if result == Z_OK { + return uncompressed.prefix(Int(uncompressedLength)) + } else if result == Z_BUF_ERROR { + // Buffer too small, try larger + uncompressedLength *= 2 + maxAttempts -= 1 + } else { + Logger.tak.debug("Zlib: uncompress failed with code \(result)") + return nil + } + } + + return nil + } +} diff --git a/Meshtastic/Helpers/TAK/FountainCodec.swift b/Meshtastic/Helpers/TAK/FountainCodec.swift new file mode 100644 index 00000000..95f559a6 --- /dev/null +++ b/Meshtastic/Helpers/TAK/FountainCodec.swift @@ -0,0 +1,616 @@ +// +// FountainCodec.swift +// Meshtastic +// +// Fountain code (LT codes) implementation for reliable transfer over lossy mesh networks +// Based on the ATAK Meshtastic plugin protocol +// + +import Foundation +import CryptoKit +import OSLog + +// MARK: - Constants + +enum FountainConstants { + /// Magic bytes identifying fountain packets: "FTN" + static let magic: [UInt8] = [0x46, 0x54, 0x4E] + + /// Maximum payload size per block + static let blockSize = 220 + + /// Header size for data blocks + static let dataHeaderSize = 11 + + /// Size threshold for fountain coding (below this, send directly) + static let fountainThreshold = 233 + + /// Transfer type: CoT event + static let transferTypeCot: UInt8 = 0x00 + + /// Transfer type: File transfer + static let transferTypeFile: UInt8 = 0x01 + + /// ACK type: Transfer complete + static let ackTypeComplete: UInt8 = 0x02 + + /// ACK type: Need more blocks + static let ackTypeNeedMore: UInt8 = 0x03 + + /// ACK packet size + static let ackPacketSize = 19 +} + +// MARK: - Fountain Packet Types + +/// A received fountain block with its metadata +struct FountainBlock { + let seed: UInt16 + var indices: Set + var payload: Data + + func copy() -> FountainBlock { + return FountainBlock(seed: seed, indices: indices, payload: payload) + } +} + +/// State for receiving a fountain-coded transfer +class FountainReceiveState { + let transferId: UInt32 + let K: Int + let totalLength: Int + var blocks: [FountainBlock] = [] + let createdAt: Date + + init(transferId: UInt32, K: Int, totalLength: Int) { + self.transferId = transferId + self.K = K + self.totalLength = totalLength + self.createdAt = Date() + } + + func addBlock(_ block: FountainBlock) { + // Don't add duplicate seeds + if !blocks.contains(where: { $0.seed == block.seed }) { + blocks.append(block) + } + } + + var isExpired: Bool { + // Expire after 60 seconds + return Date().timeIntervalSince(createdAt) > 60 + } +} + +/// Parsed fountain data block header +struct FountainDataHeader { + let transferId: UInt32 // 24-bit, stored in lower 24 bits + let seed: UInt16 + let K: UInt8 + let totalLength: UInt16 +} + +/// Parsed fountain ACK packet +struct FountainAck { + let transferId: UInt32 + let type: UInt8 + let received: UInt16 + let needed: UInt16 + let dataHash: Data +} + +// MARK: - Java-Compatible Random Number Generator + +/// Java's java.util.Random implementation (Linear Congruential Generator) +/// CRITICAL: Must match Java exactly for Android interoperability +struct JavaRandom { + private var seed: Int64 + + init(seed: Int64) { + // Java's Random constructor: (seed ^ 0x5DEECE66DL) & ((1L << 48) - 1) + self.seed = (seed ^ 0x5DEECE66D) & ((Int64(1) << 48) - 1) + } + + /// Generate next random bits (Java's protected next(int bits) method) + mutating func next(bits: Int) -> Int32 { + // seed = (seed * 0x5DEECE66DL + 0xBL) & ((1L << 48) - 1) + seed = (seed &* 0x5DEECE66D &+ 0xB) & ((Int64(1) << 48) - 1) + return Int32(truncatingIfNeeded: seed >> (48 - bits)) + } + + /// Generate random int in [0, bound) - matches Java's nextInt(int bound) + mutating func nextInt(bound: Int) -> Int { + guard bound > 0 else { return 0 } + + // Power of 2 optimization + if (bound & -bound) == bound { + return Int((Int64(bound) &* Int64(next(bits: 31))) >> 31) + } + + // Rejection sampling to avoid modulo bias + var bits: Int32 + var val: Int + repeat { + bits = next(bits: 31) + val = Int(bits) % bound + } while bits - Int32(val) + Int32(bound - 1) < 0 + + return val + } + + /// Generate random double in [0.0, 1.0) - matches Java's nextDouble() + mutating func nextDouble() -> Double { + let high = Int64(next(bits: 26)) + let low = Int64(next(bits: 27)) + return Double((high << 27) + low) / Double(Int64(1) << 53) + } +} + +// MARK: - Fountain Codec + +/// Encoder and decoder for fountain-coded transfers +final class FountainCodec { + + static let shared = FountainCodec() + + private var receiveStates: [UInt32: FountainReceiveState] = [:] + + private init() {} + + // MARK: - Transfer ID Generation + + /// Generate a unique random 24-bit transfer ID + /// CRITICAL: Must be random to avoid collisions with recent transfers + func generateTransferId() -> UInt32 { + let random = UInt32.random(in: 0...0xFFFFFF) + let time = UInt32(Date().timeIntervalSince1970) & 0xFFFF + return (random ^ time) & 0xFFFFFF + } + + // MARK: - Encoding + + /// Encode data into fountain-coded blocks + /// - Parameters: + /// - data: The data to encode (should include transfer type prefix) + /// - transferId: Unique transfer ID for this transmission + /// - Returns: Array of encoded block packets ready for transmission + func encode(data: Data, transferId: UInt32) -> [Data] { + // Guard against empty data + guard !data.isEmpty else { + Logger.tak.warning("Fountain encode: empty data") + return [] + } + + let K = max(1, Int(ceil(Double(data.count) / Double(FountainConstants.blockSize)))) + let overhead = getAdaptiveOverhead(K) + let blocksToSend = max(1, Int(ceil(Double(K) * (1.0 + overhead)))) + + // Split into source blocks (pad last block with zeros) + let sourceBlocks = splitIntoBlocks(data: data, K: K) + + // Debug: Log source block hashes to verify they're different + for (i, block) in sourceBlocks.enumerated() { + let hash = block.prefix(8).map { String(format: "%02X", $0) }.joined() + Logger.tak.debug("Fountain sourceBlock[\(i)]: first 8 bytes = \(hash)") + } + + var packets: [Data] = [] + + for i in 0.. [Data] { + var blocks: [Data] = [] + for i in 0.. Data { + var packet = Data() + + // Magic bytes + packet.append(contentsOf: FountainConstants.magic) + + // Transfer ID (24-bit, big-endian) + packet.append(UInt8((transferId >> 16) & 0xFF)) + packet.append(UInt8((transferId >> 8) & 0xFF)) + packet.append(UInt8(transferId & 0xFF)) + + // Seed (16-bit, big-endian) + packet.append(UInt8((seed >> 8) & 0xFF)) + packet.append(UInt8(seed & 0xFF)) + + // K (number of source blocks) + packet.append(K) + + // Total length (16-bit, big-endian) + packet.append(UInt8((totalLength >> 8) & 0xFF)) + packet.append(UInt8(totalLength & 0xFF)) + + // Payload + packet.append(payload) + + return packet + } + + // MARK: - Decoding + + /// Check if data is a fountain packet + static func isFountainPacket(_ data: Data) -> Bool { + guard data.count >= 3 else { return false } + return data[0] == FountainConstants.magic[0] + && data[1] == FountainConstants.magic[1] + && data[2] == FountainConstants.magic[2] + } + + /// Parse a fountain data block header + func parseDataHeader(_ data: Data) -> FountainDataHeader? { + guard data.count >= FountainConstants.dataHeaderSize else { return nil } + guard Self.isFountainPacket(data) else { return nil } + + let transferId = (UInt32(data[3]) << 16) | (UInt32(data[4]) << 8) | UInt32(data[5]) + let seed = (UInt16(data[6]) << 8) | UInt16(data[7]) + let K = data[8] + let totalLength = (UInt16(data[9]) << 8) | UInt16(data[10]) + + return FountainDataHeader(transferId: transferId, seed: seed, K: K, totalLength: totalLength) + } + + /// Handle an incoming fountain packet + /// - Parameters: + /// - data: The raw packet data + /// - senderNodeId: ID of the sending node + /// - Returns: Decoded data if transfer is complete, nil otherwise + func handleIncomingPacket(_ data: Data, senderNodeId: UInt32) -> (data: Data, transferId: UInt32)? { + // Clean up expired states + cleanupExpiredStates() + + guard let header = parseDataHeader(data) else { + Logger.tak.warning("Invalid fountain packet header") + return nil + } + + let payload = data.dropFirst(FountainConstants.dataHeaderSize) + guard payload.count == FountainConstants.blockSize else { + Logger.tak.warning("Invalid fountain payload size: \(payload.count)") + return nil + } + + // Get or create receive state + let state: FountainReceiveState + if let existing = receiveStates[header.transferId] { + state = existing + } else { + state = FountainReceiveState( + transferId: header.transferId, + K: Int(header.K), + totalLength: Int(header.totalLength) + ) + receiveStates[header.transferId] = state + Logger.tak.debug("New fountain transfer: id=\(header.transferId), K=\(header.K), len=\(header.totalLength)") + } + + // Regenerate source indices from seed + let indices = regenerateIndices(seed: header.seed, K: state.K, transferId: header.transferId) + + // Add block + let block = FountainBlock(seed: header.seed, indices: indices, payload: Data(payload)) + state.addBlock(block) + + Logger.tak.debug("Fountain block received: xferId=\(header.transferId), seed=\(header.seed), blocks=\(state.blocks.count)/\(state.K)") + + // Try to decode if we have enough blocks + if state.blocks.count >= state.K { + if let decoded = peelingDecode(state) { + // Remove completed state + receiveStates.removeValue(forKey: header.transferId) + Logger.tak.info("Fountain decode complete: \(decoded.count) bytes from \(state.blocks.count) blocks") + return (decoded, header.transferId) + } + } + + return nil + } + + /// Build an ACK packet + func buildAck(transferId: UInt32, type: UInt8, received: UInt16, needed: UInt16, dataHash: Data) -> Data { + var packet = Data() + + // Magic bytes + packet.append(contentsOf: FountainConstants.magic) + + // Transfer ID (24-bit, big-endian) + packet.append(UInt8((transferId >> 16) & 0xFF)) + packet.append(UInt8((transferId >> 8) & 0xFF)) + packet.append(UInt8(transferId & 0xFF)) + + // Type + packet.append(type) + + // Received (16-bit, big-endian) + packet.append(UInt8((received >> 8) & 0xFF)) + packet.append(UInt8(received & 0xFF)) + + // Needed (16-bit, big-endian) + packet.append(UInt8((needed >> 8) & 0xFF)) + packet.append(UInt8(needed & 0xFF)) + + // Data hash (8 bytes) + packet.append(dataHash.prefix(8)) + + return packet + } + + /// Parse an ACK packet + func parseAck(_ data: Data) -> FountainAck? { + guard data.count >= FountainConstants.ackPacketSize else { return nil } + guard Self.isFountainPacket(data) else { return nil } + + let transferId = (UInt32(data[3]) << 16) | (UInt32(data[4]) << 8) | UInt32(data[5]) + let type = data[6] + let received = (UInt16(data[7]) << 8) | UInt16(data[8]) + let needed = (UInt16(data[9]) << 8) | UInt16(data[10]) + let dataHash = Data(data[11..<19]) + + return FountainAck(transferId: transferId, type: type, received: received, needed: needed, dataHash: dataHash) + } + + // MARK: - Peeling Decoder + + /// Decode using the peeling algorithm + private func peelingDecode(_ state: FountainReceiveState) -> Data? { + var decoded: [Int: Data] = [:] + var workingBlocks = state.blocks.map { $0.copy() } + + var progress = true + while progress && decoded.count < state.K { + progress = false + + for i in 0..= state.K else { + Logger.tak.debug("Peeling decode incomplete: \(decoded.count)/\(state.K) blocks decoded") + return nil + } + + // Reassemble original data + var result = Data() + for i in 0.. Double { + if K <= 10 { return 0.50 } // 50% for very small + else if K <= 50 { return 0.25 } // 25% for small + else { return 0.15 } // 15% for larger + } + + /// Generate deterministic seed from transfer ID and block index + private func generateSeed(transferId: UInt32, blockIndex: Int) -> UInt16 { + let combined = Int(transferId) * 31337 + blockIndex * 7919 + return UInt16(combined & 0xFFFF) + } + + /// Generate indices for encoding a block + /// CRITICAL: Must match Android's exact algorithm for interoperability + /// Android uses Java's java.util.Random (LCG) with specific block 0 handling + private func generateBlockIndices(seed: UInt16, K: Int, blockIndex: Int) -> Set { + var rng = JavaRandom(seed: Int64(seed)) + + // ALWAYS sample degree first (advances RNG state) - matches Android + let sampledDegree = sampleRobustSolitonDegree(&rng, K: K) + + // For block 0: ignore sampled degree, use degree=1 instead + // For other blocks: use the sampled degree + // This matches Android's isFirstBlock logic + let degree = (blockIndex == 0) ? 1 : sampledDegree + + // Select indices with RNG now advanced past degree sampling + return selectIndices(&rng, K: K, degree: degree) + } + + /// Regenerate source indices from seed (must match sender's algorithm) + /// CRITICAL: Must use same RNG flow as generateBlockIndices for Android interop + private func regenerateIndices(seed: UInt16, K: Int, transferId: UInt32) -> Set { + var rng = JavaRandom(seed: Int64(seed)) + + // ALWAYS sample degree first (advances RNG state) - matches Android + let sampledDegree = sampleRobustSolitonDegree(&rng, K: K) + + // Check if this is block 0 (forced degree=1) + let expectedSeed0 = generateSeed(transferId: transferId, blockIndex: 0) + let degree = (seed == expectedSeed0) ? 1 : sampledDegree + + // Select indices with RNG now advanced past degree sampling + return selectIndices(&rng, K: K, degree: degree) + } + + /// Select source block indices using provided RNG + /// Matches Android's selectIndices algorithm exactly + private func selectIndices(_ rng: inout JavaRandom, K: Int, degree: Int) -> Set { + var indices = Set() + + // Select 'degree' unique indices + while indices.count < degree && indices.count < K { + let idx = rng.nextInt(bound: K) + indices.insert(idx) + } + + return indices + } + + /// Sample degree from Robust Soliton distribution using provided RNG + /// Matches Android's sampleDegree algorithm exactly + private func sampleRobustSolitonDegree(_ rng: inout JavaRandom, K: Int) -> Int { + let cdf = buildRobustSolitonCDF(K: K) + let u = rng.nextDouble() + + for d in 1...K { + if u <= cdf[d] { + return d + } + } + return K + } + + /// Build CDF for Robust Soliton distribution + private func buildRobustSolitonCDF(K: Int, c: Double = 0.1, delta: Double = 0.5) -> [Double] { + // Guard against K <= 0 + guard K > 0 else { + return [1.0] // Single element CDF + } + + // Ideal Soliton distribution + var rho = [Double](repeating: 0, count: K + 1) + rho[1] = 1.0 / Double(K) + for d in 2...K { + rho[d] = 1.0 / (Double(d) * Double(d - 1)) + } + + // Robust Soliton addition (tau) + let R = c * log(Double(K) / delta) * sqrt(Double(K)) + var tau = [Double](repeating: 0, count: K + 1) + let threshold = Int(Double(K) / R) + + for d in 1...K { + if d < threshold { + tau[d] = R / (Double(d) * Double(K)) + } else if d == threshold { + tau[d] = R * log(R / delta) / Double(K) + } + } + + // Combine and normalize + var mu = [Double](repeating: 0, count: K + 1) + var sum = 0.0 + for d in 1...K { + mu[d] = rho[d] + tau[d] + sum += mu[d] + } + + // Build CDF + var cdf = [Double](repeating: 0, count: K + 1) + var cumulative = 0.0 + for d in 1...K { + cumulative += mu[d] / sum + cdf[d] = cumulative + } + + return cdf + } + + /// XOR two data blocks + private func xor(_ a: Data, _ b: Data) -> Data { + // IMPORTANT: Rebase inputs to ensure 0-based indices + // Data slices keep original indices which causes crashes when accessing [i] + let aData = a.startIndex == 0 ? a : Data(a) + let bData = b.startIndex == 0 ? b : Data(b) + + var result = Data(count: max(aData.count, bData.count)) + for i in 0.. Data { + let digest = SHA256.hash(data: data) + return Data(digest.prefix(8)) + } + + /// Clean up expired receive states + private func cleanupExpiredStates() { + let expiredIds = receiveStates.filter { $0.value.isExpired }.map { $0.key } + for id in expiredIds { + receiveStates.removeValue(forKey: id) + Logger.tak.debug("Cleaned up expired fountain state: \(id)") + } + } +} diff --git a/Meshtastic/Helpers/TAK/GenericCoTHandler.swift b/Meshtastic/Helpers/TAK/GenericCoTHandler.swift new file mode 100644 index 00000000..6ed357fd --- /dev/null +++ b/Meshtastic/Helpers/TAK/GenericCoTHandler.swift @@ -0,0 +1,399 @@ +// +// GenericCoTHandler.swift +// Meshtastic +// +// Handles generic CoT events that don't map to TAKPacket protobuf +// Uses EXI compression and Fountain codes for reliable transfer +// + +import Foundation +import MeshtasticProtobufs +import OSLog + +/// Port numbers for TAK communication +enum TAKPortNum: UInt32 { + /// TAKPacket protobuf (PLI, GeoChat) - small, structured messages + case atakPlugin = 72 + + /// EXI-compressed CoT XML - generic/large messages, fountain coded + case atakForwarder = 257 +} + +/// Handler for generic CoT events over the mesh network +@MainActor +final class GenericCoTHandler { + + static let shared = GenericCoTHandler() + + weak var accessoryManager: AccessoryManager? + + /// Pending outgoing fountain transfers awaiting ACK + private var pendingTransfers: [UInt32: PendingTransfer] = [:] + + private init() {} + + // MARK: - Outgoing CoT Classification + + /// Determine how a CoT message should be sent + enum CoTSendMethod { + /// Use TAKPacket.pli on ATAK_PLUGIN port + case takPacketPLI + /// Use TAKPacket.chat on ATAK_PLUGIN port + case takPacketChat + /// Use EXI compression on ATAK_FORWARDER port (small, no fountain) + case exiDirect + /// Use EXI + Fountain coding on ATAK_FORWARDER port (large) + case exiFountain + } + + /// Classify a CoT message to determine send method + func classifySendMethod(for cot: CoTMessage) -> CoTSendMethod { + // Self PLI (position) + if cot.type.hasPrefix("a-f-G") || cot.type.hasPrefix("a-f-g") { + return .takPacketPLI + } + + // GeoChat + if cot.type == "b-t-f" { + return .takPacketChat + } + + // Everything else goes through EXI/Forwarder + // Check compressed size to determine if fountain coding needed + let xml = cot.toXML() + if let compressed = EXICodec.shared.compress(xml) { + // +1 for transfer type byte + if compressed.count + 1 < FountainConstants.fountainThreshold { + return .exiDirect + } else { + return .exiFountain + } + } + + // Fallback to direct (compression failed, use raw) + return .exiDirect + } + + // MARK: - Sending Generic CoT + + /// Send a generic CoT event (markers, shapes, routes, etc.) + /// - Parameters: + /// - cot: The CoT message to send + /// - channel: Meshtastic channel (0 = primary) + func sendGenericCoT(_ cot: CoTMessage, channel: UInt32 = 0) async throws { + guard let accessoryManager else { + throw GenericCoTError.notConnected + } + + guard accessoryManager.isConnected else { + throw GenericCoTError.notConnected + } + + // Compress to EXI + let xml = cot.toXML() + guard let exiData = EXICodec.shared.compress(xml) else { + throw GenericCoTError.compressionFailed + } + + // Prepend transfer type + var payload = Data([FountainConstants.transferTypeCot]) + payload.append(exiData) + + Logger.tak.debug("Generic CoT: type=\(cot.type), xml=\(xml.count)B, compressed=\(payload.count)B") + + // Check if small enough to send directly + if payload.count < FountainConstants.fountainThreshold { + try await sendDirect(payload, channel: channel) + } else { + try await sendFountainCoded(payload, channel: channel) + } + } + + /// Send small payload directly (no fountain coding) + private func sendDirect(_ payload: Data, channel: UInt32) async throws { + guard let accessoryManager, let activeConnection = accessoryManager.activeConnection else { + throw GenericCoTError.notConnected + } + + guard let deviceNum = activeConnection.device.num else { + throw GenericCoTError.noDeviceNumber + } + + var dataMessage = DataMessage() + dataMessage.portnum = .atakForwarder // Port 257 + dataMessage.payload = payload + + var meshPacket = MeshPacket() + meshPacket.to = 0xFFFFFFFF // Broadcast + meshPacket.from = UInt32(deviceNum) + meshPacket.channel = channel + meshPacket.id = UInt32.random(in: UInt32(UInt8.max).. CoTMessage? { + guard case let .decoded(data) = packet.payloadVariant else { + Logger.tak.warning("ATAK_FORWARDER packet without decoded payload") + return nil + } + + let payload = data.payload + guard !payload.isEmpty else { + Logger.tak.warning("Empty ATAK_FORWARDER payload") + return nil + } + + // Check if this is a fountain packet (starts with "FTN" magic) + if FountainCodec.isFountainPacket(payload) { + // Distinguish between ACK (19 bytes) and data block (231 bytes) + // ACK: magic(3) + transferId(3) + type(1) + received(2) + needed(2) + hash(8) = 19 + // Data: magic(3) + transferId(3) + seed(2) + K(1) + totalLen(2) + payload(220) = 231 + if payload.count == FountainConstants.ackPacketSize { + // This is a fountain ACK - handle it and return nil (no CoT to forward) + handleIncomingAck(payload, from: packet.from) + return nil + } + return handleFountainPacket(payload, from: packet.from) + } + + // Direct packet (not fountain coded) + return handleDirectPacket(payload, from: packet.from) + } + + /// Handle direct (non-fountain) packet + private func handleDirectPacket(_ payload: Data, from nodeNum: UInt32) -> CoTMessage? { + guard payload.count > 1 else { + Logger.tak.warning("Direct packet too short: \(payload.count) bytes") + return nil + } + + let transferType = payload[0] + let exiData = payload.dropFirst() + + guard transferType == FountainConstants.transferTypeCot else { + Logger.tak.debug("Ignoring non-CoT transfer type: \(transferType)") + return nil + } + + // Decompress EXI to XML + guard let xml = EXICodec.shared.decompress(Data(exiData)) else { + Logger.tak.warning("Failed to decompress EXI data from node \(nodeNum)") + return nil + } + + // Parse CoT XML + guard let cot = CoTMessage.parse(from: xml) else { + Logger.tak.warning("Failed to parse CoT XML from node \(nodeNum)") + return nil + } + + Logger.tak.info("Received generic CoT from node \(nodeNum): \(cot.type)") + return cot + } + + /// Handle fountain-coded packet + private func handleFountainPacket(_ payload: Data, from nodeNum: UInt32) -> CoTMessage? { + // Pass to fountain codec + guard let (decodedData, transferId) = FountainCodec.shared.handleIncomingPacket(payload, senderNodeId: nodeNum) else { + // Not yet complete, waiting for more blocks + return nil + } + + // Transfer complete - send ACK (twice for redundancy) + let hash = FountainCodec.computeHash(decodedData) + Task { + await sendFountainAck(transferId: transferId, hash: hash, to: nodeNum) + try? await Task.sleep(nanoseconds: 50_000_000) // 50ms delay + await sendFountainAck(transferId: transferId, hash: hash, to: nodeNum) + } + + // Extract transfer type and data + guard decodedData.count > 1 else { + Logger.tak.warning("Decoded fountain data too short") + return nil + } + + let transferType = decodedData[0] + let exiData = decodedData.dropFirst() + + guard transferType == FountainConstants.transferTypeCot else { + Logger.tak.debug("Ignoring non-CoT fountain transfer type: \(transferType)") + return nil + } + + // Decompress EXI to XML + guard let xml = EXICodec.shared.decompress(Data(exiData)) else { + Logger.tak.warning("Failed to decompress fountain EXI data") + return nil + } + + // Parse CoT XML + guard let cot = CoTMessage.parse(from: xml) else { + Logger.tak.warning("Failed to parse fountain CoT XML") + return nil + } + + Logger.tak.info("Received fountain-coded CoT from node \(nodeNum): \(cot.type)") + return cot + } + + /// Send fountain ACK + private func sendFountainAck(transferId: UInt32, hash: Data, to nodeNum: UInt32) async { + guard let accessoryManager, let activeConnection = accessoryManager.activeConnection else { + return + } + + guard let deviceNum = activeConnection.device.num else { + return + } + + let ackPacket = FountainCodec.shared.buildAck( + transferId: transferId, + type: FountainConstants.ackTypeComplete, + received: 0, + needed: 0, + dataHash: hash + ) + + var dataMessage = DataMessage() + dataMessage.portnum = .atakForwarder + dataMessage.payload = ackPacket + + var meshPacket = MeshPacket() + meshPacket.to = nodeNum + meshPacket.from = UInt32(deviceNum) + meshPacket.id = UInt32.random(in: UInt32(UInt8.max).. Bool { + let query: [String: Any] = [ + kSecClass as String: kSecClassIdentity, + kSecAttrLabel as String: serverIdentityCustomTag, + kSecReturnRef as String: true + ] + var item: CFTypeRef? + return SecItemCopyMatching(query as CFDictionary, &item) == errSecSuccess + } + + /// Get the bundled CA certificate data for sharing to ITAK + func getBundledCACertificateData() -> Data? { + let pemURL = Bundle.main.url(forResource: "ca", withExtension: "pem", subdirectory: "Certificates") + ?? Bundle.main.url(forResource: "ca", withExtension: "pem") + + guard let url = pemURL, let pemData = try? Data(contentsOf: url) else { + return nil + } + return pemData + } + + /// Get URL to bundled CA certificate for sharing + func getBundledCACertificateURL() -> URL? { + return Bundle.main.url(forResource: "ca", withExtension: "pem", subdirectory: "Certificates") + ?? Bundle.main.url(forResource: "ca", withExtension: "pem") + } + + /// Get the bundled server P12 data for sharing to ITAK (used as truststore) + func getBundledServerP12Data() -> Data? { + let p12URL = Bundle.main.url(forResource: "server", withExtension: "p12", subdirectory: "Certificates") + ?? Bundle.main.url(forResource: "server", withExtension: "p12") + + guard let url = p12URL, let p12Data = try? Data(contentsOf: url) else { + return nil + } + return p12Data + } + + /// Get the password for bundled certificates (for data package) + func getBundledCertificatePassword() -> String { + return bundledPassword + } + + /// Get the bundled client P12 data for sharing to ITAK (for mutual TLS) + func getBundledClientP12Data() -> Data? { + let p12URL = Bundle.main.url(forResource: "client", withExtension: "p12", subdirectory: "Certificates") + ?? Bundle.main.url(forResource: "client", withExtension: "p12") + + guard let url = p12URL, let p12Data = try? Data(contentsOf: url) else { + return nil + } + return p12Data + } + + /// Check if a bundled client certificate exists + func hasBundledClientCertificate() -> Bool { + return getBundledClientP12Data() != nil + } + + // MARK: - Active Certificate Data (for Data Package) + + /// Get the active server P12 data (custom if available, otherwise bundled) + /// Used for generating data packages + func getActiveServerP12Data() -> Data? { + // Check for custom certificate first + if hasCustomServerCertificate(), + let customData = UserDefaults.standard.data(forKey: customServerP12DataKey) { + Logger.tak.debug("Using custom server P12 for data package") + return customData + } + // Fall back to bundled + Logger.tak.debug("Using bundled server P12 for data package") + return getBundledServerP12Data() + } + + /// Get the active client P12 data (custom if available, otherwise bundled) + /// Used for generating data packages + func getActiveClientP12Data() -> Data? { + // Check for custom certificate first + if let customData = UserDefaults.standard.data(forKey: customClientP12DataKey) { + Logger.tak.debug("Using custom client P12 for data package") + return customData + } + // Fall back to bundled + Logger.tak.debug("Using bundled client P12 for data package") + return getBundledClientP12Data() + } + + /// Get the password for the active server certificate + func getActiveServerCertificatePassword() -> String { + if hasCustomServerCertificate(), + let customPassword = UserDefaults.standard.string(forKey: customServerP12PasswordKey) { + return customPassword + } + return bundledPassword + } + + /// Get the password for the active client certificate + func getActiveClientCertificatePassword() -> String { + if let customPassword = UserDefaults.standard.string(forKey: customClientP12PasswordKey) { + return customPassword + } + return bundledPassword + } + + /// Import a custom client P12 certificate (for data package generation) + func importCustomClientP12(data: Data, password: String) { + UserDefaults.standard.set(data, forKey: customClientP12DataKey) + UserDefaults.standard.set(password, forKey: customClientP12PasswordKey) + Logger.tak.info("Custom client P12 imported for data package") + } + + /// Check if custom client P12 is available + func hasCustomClientP12() -> Bool { + return UserDefaults.standard.data(forKey: customClientP12DataKey) != nil + } + + /// Clear custom certificate data (called when resetting to defaults) + private func clearCustomCertificateData() { + UserDefaults.standard.removeObject(forKey: customServerP12DataKey) + UserDefaults.standard.removeObject(forKey: customServerP12PasswordKey) + UserDefaults.standard.removeObject(forKey: customClientP12DataKey) + UserDefaults.standard.removeObject(forKey: customClientP12PasswordKey) + Logger.tak.debug("Cleared custom certificate data") + } + + // MARK: - Server Identity (PKCS#12) + + /// Import server identity from PKCS#12 (.p12) file data + /// - Parameters: + /// - p12Data: The raw PKCS#12 file data + /// - password: Password to decrypt the PKCS#12 file + /// - isCustom: Whether this is a user-imported custom certificate (default: true) + /// - Returns: The imported SecIdentity + func importServerIdentity(from p12Data: Data, password: String, isCustom: Bool = true) throws -> SecIdentity { + let options: [String: Any] = [kSecImportExportPassphrase as String: password] + var items: CFArray? + + let status = SecPKCS12Import(p12Data as CFData, options as CFDictionary, &items) + + guard status == errSecSuccess else { + Logger.tak.error("Failed to import PKCS#12: \(status)") + throw TAKCertificateError.importFailed(status) + } + + guard let itemArray = items as? [[String: Any]], + let firstItem = itemArray.first, + let identity = firstItem[kSecImportItemIdentity as String] as! SecIdentity? else { + throw TAKCertificateError.noIdentityFound + } + + // Store in Keychain for persistence + try storeServerIdentity(identity, isCustom: isCustom) + + // Store the raw P12 data and password for data package generation (only for custom certs) + if isCustom { + UserDefaults.standard.set(p12Data, forKey: customServerP12DataKey) + UserDefaults.standard.set(password, forKey: customServerP12PasswordKey) + Logger.tak.debug("Stored custom server P12 data for data package generation") + } + + Logger.tak.info("Server identity imported successfully (custom: \(isCustom))") + return identity + } + + /// Store server identity in Keychain + private func storeServerIdentity(_ identity: SecIdentity, isCustom: Bool = true) throws { + let tag = isCustom ? serverIdentityCustomTag : serverIdentityTag + + // First delete any existing identity with this tag + let deleteQuery: [String: Any] = [ + kSecClass as String: kSecClassIdentity, + kSecAttrLabel as String: tag + ] + SecItemDelete(deleteQuery as CFDictionary) + + // If storing custom cert, also delete the bundled one (custom takes precedence) + if isCustom { + let deleteBundledQuery: [String: Any] = [ + kSecClass as String: kSecClassIdentity, + kSecAttrLabel as String: serverIdentityTag + ] + SecItemDelete(deleteBundledQuery as CFDictionary) + } + + // Add new identity + let addQuery: [String: Any] = [ + kSecValueRef as String: identity, + kSecAttrLabel as String: tag, + kSecAttrAccessible as String: kSecAttrAccessibleAfterFirstUnlock + ] + + let status = SecItemAdd(addQuery as CFDictionary, nil) + guard status == errSecSuccess else { + Logger.tak.error("Failed to store server identity in Keychain: \(status)") + throw TAKCertificateError.keychainError(status) + } + } + + /// Retrieve stored server identity from Keychain + /// Custom certificates take precedence over bundled ones + func getServerIdentity() -> SecIdentity? { + // First try custom certificate + let customQuery: [String: Any] = [ + kSecClass as String: kSecClassIdentity, + kSecAttrLabel as String: serverIdentityCustomTag, + kSecReturnRef as String: true + ] + + var item: CFTypeRef? + var status = SecItemCopyMatching(customQuery as CFDictionary, &item) + + if status == errSecSuccess { + return (item as! SecIdentity) + } + + // Fall back to bundled certificate + let bundledQuery: [String: Any] = [ + kSecClass as String: kSecClassIdentity, + kSecAttrLabel as String: serverIdentityTag, + kSecReturnRef as String: true + ] + + status = SecItemCopyMatching(bundledQuery as CFDictionary, &item) + + guard status == errSecSuccess else { + if status != errSecItemNotFound { + Logger.tak.warning("Failed to retrieve server identity: \(status)") + } + return nil + } + + return (item as! SecIdentity) + } + + /// Check if server certificate is configured + func hasServerCertificate() -> Bool { + return getServerIdentity() != nil + } + + /// Delete custom server identity and reload bundled default + func deleteServerIdentity() { + // Delete custom certificate + let customQuery: [String: Any] = [ + kSecClass as String: kSecClassIdentity, + kSecAttrLabel as String: serverIdentityCustomTag + ] + let customStatus = SecItemDelete(customQuery as CFDictionary) + + // Delete bundled certificate too + let bundledQuery: [String: Any] = [ + kSecClass as String: kSecClassIdentity, + kSecAttrLabel as String: serverIdentityTag + ] + let bundledStatus = SecItemDelete(bundledQuery as CFDictionary) + + if customStatus == errSecSuccess || bundledStatus == errSecSuccess { + Logger.tak.info("Server identity deleted") + } + + // Reload bundled default + loadBundledServerIdentity() + } + + /// Reset to bundled default certificate (deletes custom certificate) + func resetToDefaultServerCertificate() { + // Clear custom certificate data from UserDefaults + clearCustomCertificateData() + + // Delete custom certificate + let customQuery: [String: Any] = [ + kSecClass as String: kSecClassIdentity, + kSecAttrLabel as String: serverIdentityCustomTag + ] + SecItemDelete(customQuery as CFDictionary) + + // Delete existing bundled and reload + let bundledQuery: [String: Any] = [ + kSecClass as String: kSecClassIdentity, + kSecAttrLabel as String: serverIdentityTag + ] + SecItemDelete(bundledQuery as CFDictionary) + + loadBundledServerIdentity() + Logger.tak.info("Reset to bundled default server certificate") + } + + /// Get certificate info for display purposes + func getServerCertificateInfo() -> String? { + guard let identity = getServerIdentity() else { return nil } + + var certificate: SecCertificate? + let status = SecIdentityCopyCertificate(identity, &certificate) + guard status == errSecSuccess, let cert = certificate else { return nil } + + let isCustom = hasCustomServerCertificate() + let prefix = isCustom ? "Custom: " : "Default: " + + if let summary = SecCertificateCopySubjectSummary(cert) as String? { + return prefix + summary + } + + return prefix + "Certificate loaded" + } + + // MARK: - Client CA Certificates (PEM) + + /// Import client CA certificate from PEM file data + /// - Parameter pemData: The raw PEM file data + /// - Returns: The imported SecCertificate + func importClientCACertificate(from pemData: Data) throws -> SecCertificate { + // Extract DER data from PEM format + let derData = try extractDERFromPEM(pemData) + + guard let certificate = SecCertificateCreateWithData(nil, derData as CFData) else { + throw TAKCertificateError.invalidCertificate + } + + // Store in Keychain + try storeClientCACertificate(certificate) + + Logger.tak.info("Client CA certificate imported successfully") + return certificate + } + + /// Extract DER-encoded certificate data from PEM format + private func extractDERFromPEM(_ pemData: Data) throws -> Data { + guard let pemString = String(data: pemData, encoding: .utf8) else { + throw TAKCertificateError.invalidPEM + } + + // Remove PEM headers and whitespace + let base64 = pemString + .replacingOccurrences(of: "-----BEGIN CERTIFICATE-----", with: "") + .replacingOccurrences(of: "-----END CERTIFICATE-----", with: "") + .replacingOccurrences(of: "\n", with: "") + .replacingOccurrences(of: "\r", with: "") + .trimmingCharacters(in: .whitespaces) + + guard let derData = Data(base64Encoded: base64) else { + throw TAKCertificateError.invalidPEM + } + + return derData + } + + /// Store client CA certificate in Keychain + private func storeClientCACertificate(_ certificate: SecCertificate) throws { + let addQuery: [String: Any] = [ + kSecClass as String: kSecClassCertificate, + kSecValueRef as String: certificate, + kSecAttrLabel as String: clientCATag, + kSecAttrAccessible as String: kSecAttrAccessibleAfterFirstUnlock + ] + + let status = SecItemAdd(addQuery as CFDictionary, nil) + + // Ignore duplicate item errors (certificate already imported) + guard status == errSecSuccess || status == errSecDuplicateItem else { + Logger.tak.error("Failed to store client CA certificate: \(status)") + throw TAKCertificateError.keychainError(status) + } + } + + /// Get all stored client CA certificates + func getClientCACertificates() -> [SecCertificate] { + let query: [String: Any] = [ + kSecClass as String: kSecClassCertificate, + kSecAttrLabel as String: clientCATag, + kSecReturnRef as String: true, + kSecMatchLimit as String: kSecMatchLimitAll + ] + + var items: CFTypeRef? + let status = SecItemCopyMatching(query as CFDictionary, &items) + + guard status == errSecSuccess else { + if status != errSecItemNotFound { + Logger.tak.warning("Failed to retrieve client CA certificates: \(status)") + } + return [] + } + + // Handle both single item and array returns + if let certificates = items as? [SecCertificate] { + return certificates + } else if let certificate = items as! SecCertificate? { + return [certificate] + } + + return [] + } + + /// Check if at least one client CA certificate is configured + func hasClientCACertificate() -> Bool { + return !getClientCACertificates().isEmpty + } + + /// Delete all client CA certificates from Keychain + func deleteClientCACertificates() { + let query: [String: Any] = [ + kSecClass as String: kSecClassCertificate, + kSecAttrLabel as String: clientCATag + ] + let status = SecItemDelete(query as CFDictionary) + if status == errSecSuccess || status == errSecItemNotFound { + Logger.tak.info("Client CA certificates deleted") + } + } + + /// Get info about stored client CA certificates for display + func getClientCACertificateInfo() -> [String] { + let certificates = getClientCACertificates() + return certificates.compactMap { cert in + SecCertificateCopySubjectSummary(cert) as String? + } + } + + // MARK: - Certificate Validation + + /// Validate a client certificate against the stored CA certificates + func validateClientCertificate(_ trust: SecTrust) -> Bool { + let caCertificates = getClientCACertificates() + + guard !caCertificates.isEmpty else { + Logger.tak.warning("No client CA certificates configured for validation") + return false + } + + // Set the anchor certificates (trusted CAs) + SecTrustSetAnchorCertificates(trust, caCertificates as CFArray) + SecTrustSetAnchorCertificatesOnly(trust, true) + + var error: CFError? + let isValid = SecTrustEvaluateWithError(trust, &error) + + if !isValid { + Logger.tak.warning("Client certificate validation failed: \(error?.localizedDescription ?? "unknown")") + } + + return isValid + } +} + +// MARK: - Certificate Errors + +enum TAKCertificateError: LocalizedError { + case importFailed(OSStatus) + case noIdentityFound + case invalidCertificate + case invalidPEM + case keychainError(OSStatus) + case certificateExpired + case certificateNotYetValid + + var errorDescription: String? { + switch self { + case .importFailed(let status): + return "Failed to import PKCS#12: \(securityErrorMessage(status))" + case .noIdentityFound: + return "No identity (certificate + private key) found in PKCS#12 file" + case .invalidCertificate: + return "Invalid certificate data" + case .invalidPEM: + return "Invalid PEM format - ensure file contains a valid certificate" + case .keychainError(let status): + return "Keychain error: \(securityErrorMessage(status))" + case .certificateExpired: + return "Certificate has expired" + case .certificateNotYetValid: + return "Certificate is not yet valid" + } + } + + private func securityErrorMessage(_ status: OSStatus) -> String { + if let message = SecCopyErrorMessageString(status, nil) { + return message as String + } + return "Error code: \(status)" + } +} diff --git a/Meshtastic/Helpers/TAK/TAKConnection.swift b/Meshtastic/Helpers/TAK/TAKConnection.swift new file mode 100644 index 00000000..52d504e9 --- /dev/null +++ b/Meshtastic/Helpers/TAK/TAKConnection.swift @@ -0,0 +1,496 @@ +// +// TAKConnection.swift +// Meshtastic +// +// Created by niccellular 12/26/25 +// + +import Foundation +import Network +import OSLog + +/// Actor managing a single TAK client TLS connection +/// Handles CoT XML streaming protocol (messages delimited by ) +/// Implements TAK Protocol negotiation and keepalive +actor TAKConnection { + private let connection: NWConnection + private var messageBuffer = Data() + private var readerTask: Task? + private var keepaliveTask: Task? + private var continuation: AsyncStream.Continuation? + + // CoT XML message delimiters (from StreamingCotProtocol.java) + private let startTag = " AsyncStream { + AsyncStream { continuation in + self.continuation = continuation + + continuation.onTermination = { [weak self] _ in + Task { [weak self] in + await self?.disconnect() + } + } + + // Set up state handler + connection.stateUpdateHandler = { [weak self] state in + guard let self else { return } + Task { + await self.handleStateChange(state) + } + } + + // Start the connection + connection.start(queue: DispatchQueue(label: "tak.connection.\(UUID().uuidString)")) + } + } + + /// Handle connection state changes + private func handleStateChange(_ state: NWConnection.State) { + switch state { + case .ready: + isConnected = true + Logger.tak.info("TAK client connected: \(self.connection.endpoint.debugDescription)") + + // Extract client certificate info if available + extractClientInfo() + + // Notify connected + let info = clientInfo ?? TAKClientInfo(endpoint: connection.endpoint, connectedAt: Date()) + continuation?.yield(.connected(info)) + + // Send protocol support advertisement + Task { + await sendProtocolSupport() + } + + // Start reading data + startReading() + + // Start keepalive task + startKeepalive() + + case .failed(let error): + Logger.tak.error("TAK connection failed: \(error.localizedDescription)") + isConnected = false + continuation?.yield(.error(error)) + continuation?.yield(.disconnected) + continuation?.finish() + + case .cancelled: + Logger.tak.info("TAK connection cancelled") + isConnected = false + continuation?.yield(.disconnected) + continuation?.finish() + + case .waiting(let error): + Logger.tak.warning("TAK connection waiting: \(error.localizedDescription)") + + case .preparing: + Logger.tak.debug("TAK connection preparing") + + case .setup: + Logger.tak.debug("TAK connection setup") + + @unknown default: + break + } + } + + /// Extract client information from the TLS session + private func extractClientInfo() { + // Client callsign/uid will be updated when first CoT message is received + // For now just create basic client info with endpoint + clientInfo = TAKClientInfo( + endpoint: connection.endpoint, + callsign: nil, + uid: nil, + connectedAt: Date() + ) + Logger.tak.info("TAK client connected from: \(self.connection.endpoint.debugDescription)") + } + + /// Start the reader task to continuously read from the connection + private func startReading() { + readerTask = Task { + while !Task.isCancelled && isConnected { + do { + let data = try await receiveData() + if !data.isEmpty { + processReceivedData(data) + } + } catch { + if !Task.isCancelled { + Logger.tak.error("TAK read error: \(error.localizedDescription)") + continuation?.yield(.error(error)) + continuation?.yield(.disconnected) + } + break + } + } + } + } + + /// Receive data from the connection + private func receiveData() async throws -> Data { + try await withCheckedThrowingContinuation { cont in + connection.receive(minimumIncompleteLength: 1, maximumLength: 65536) { content, _, isComplete, error in + if let error { + cont.resume(throwing: error) + return + } + if isComplete { + cont.resume(throwing: TAKConnectionError.connectionClosed) + return + } + if let content { + cont.resume(returning: content) + } else { + cont.resume(returning: Data()) + } + } + } + } + + /// Process received data using streaming CoT protocol + /// Based on StreamingCotProtocol.java parsing logic from TAK Server + private func processReceivedData(_ newData: Data) { + messageBuffer.append(newData) + + // Search for complete CoT messages (delimited by ) + while let endRange = messageBuffer.range(of: endTag) { + // Find the start tag before this end tag + guard let startRange = messageBuffer.range(of: startTag) else { + // No start tag found, discard data up to end tag + Logger.tak.warning("CoT end tag without start tag, discarding") + messageBuffer.removeSubrange(.. maxMessageSize { + Logger.tak.warning("Message buffer exceeded limit (\(self.messageBuffer.count) bytes), clearing") + messageBuffer.removeAll() + } + } + + /// Parse XML data and yield the message event + private func parseAndYieldMessage(_ data: Data) { + // Log raw XML for debugging + if let xmlString = String(data: data, encoding: .utf8) { + Logger.tak.debug("=== Received CoT XML (\(data.count) bytes) ===") + Logger.tak.debug("\(xmlString)") + Logger.tak.debug("=== End Raw XML ===") + } + + do { + let cotMessage = try CoTMessage.parse(from: data) + + // Handle TAK Protocol control messages + if cotMessage.type.hasPrefix("t-x-takp") { + Logger.tak.debug("Handling TAK Protocol control message: \(cotMessage.type)") + Task { + await handleProtocolControl(cotMessage) + } + return // Don't forward control messages to app + } + + // Handle ping/pong messages (don't forward, just acknowledge) + if cotMessage.type == "t-x-c-t" || cotMessage.uid == "ping" { + Logger.tak.debug("Received ping from client") + return + } + + // Update client info if we got contact details + if let contact = cotMessage.contact { + if clientInfo?.callsign == nil { + clientInfo?.callsign = contact.callsign + } + if clientInfo?.uid == nil { + clientInfo?.uid = cotMessage.uid + } + // Update the connected event with new info + if let info = clientInfo { + continuation?.yield(.clientInfoUpdated(info)) + } + } + + Logger.tak.info("Received CoT message: type=\(cotMessage.type), uid=\(cotMessage.uid)") + Logger.tak.debug(" contact: \(cotMessage.contact?.callsign ?? "nil")") + Logger.tak.debug(" lat/lon: \(cotMessage.latitude), \(cotMessage.longitude)") + continuation?.yield(.message(cotMessage)) + + } catch { + Logger.tak.warning("Failed to parse CoT message: \(error.localizedDescription)") + // Log the raw XML for debugging + if let xmlString = String(data: data, encoding: .utf8) { + Logger.tak.debug("Failed Raw CoT XML: \(xmlString.prefix(500))") + } + } + } + + // MARK: - Protocol Negotiation + + /// Send TAK Protocol Support advertisement to client + /// This tells the client what protocol versions we support (Version 0 = XML only) + private func sendProtocolSupport() async { + let now = ISO8601DateFormatter().string(from: Date()) + let stale = ISO8601DateFormatter().string(from: Date().addingTimeInterval(60)) + + // TAK Protocol Support message - advertise version 0 (XML) only + // Type t-x-takp-v indicates TAK Protocol version advertisement + let xml = """ + + + + + + + + + """ + + do { + try await sendRawXML(xml) + Logger.tak.info("Sent TakProtocolSupport to client (version 0 - XML)") + } catch { + Logger.tak.error("Failed to send TakProtocolSupport: \(error.localizedDescription)") + } + } + + /// Handle TAK Protocol control messages (TakRequest, etc.) + private func handleProtocolControl(_ cotMessage: CoTMessage) async { + // Check for protocol request in the raw XML + // Type t-x-takp-q is a protocol request from client + if cotMessage.type == "t-x-takp-q" { + await sendProtocolResponse(accepted: true) + } + } + + /// Send protocol response to client + private func sendProtocolResponse(accepted: Bool) async { + let now = ISO8601DateFormatter().string(from: Date()) + let stale = ISO8601DateFormatter().string(from: Date().addingTimeInterval(60)) + + // Type t-x-takp-r is TAK Protocol response + let xml = """ + + + + + + + + + """ + + do { + try await sendRawXML(xml) + protocolNegotiated = true + Logger.tak.info("Sent TakResponse (accepted: \(accepted))") + } catch { + Logger.tak.error("Failed to send TakResponse: \(error.localizedDescription)") + } + } + + // MARK: - Keepalive + + /// Start the keepalive task to send periodic pings + private func startKeepalive() { + keepaliveTask = Task { + while !Task.isCancelled && isConnected { + do { + try await Task.sleep(nanoseconds: keepaliveInterval) + if isConnected { + await sendKeepalive() + } + } catch { + break + } + } + } + } + + /// Send a keepalive/ping message to client + private func sendKeepalive() async { + let now = ISO8601DateFormatter().string(from: Date()) + let stale = ISO8601DateFormatter().string(from: Date().addingTimeInterval(120)) + + // t-x-c-t is a ping/keepalive type, t-x-d-d is also used for takPong + let xml = """ + + + + + """ + + do { + try await sendRawXML(xml) + Logger.tak.debug("Sent keepalive to client") + } catch { + Logger.tak.warning("Failed to send keepalive: \(error.localizedDescription)") + } + } + + // MARK: - Send Methods + + /// Send raw XML string to the client + private func sendRawXML(_ xml: String) async throws { + guard isConnected else { + throw TAKConnectionError.notConnected + } + + guard let data = xml.data(using: .utf8) else { + throw TAKConnectionError.encodingFailed + } + + try await withCheckedThrowingContinuation { (cont: CheckedContinuation) in + connection.send(content: data, completion: .contentProcessed { error in + if let error { + cont.resume(throwing: error) + } else { + cont.resume() + } + }) + } + } + + /// Send a CoT message to this client + func send(_ cotMessage: CoTMessage) async throws { + guard isConnected else { + throw TAKConnectionError.notConnected + } + + let xml = cotMessage.toXML() + guard let data = xml.data(using: .utf8) else { + throw TAKConnectionError.encodingFailed + } + + try await withCheckedThrowingContinuation { (cont: CheckedContinuation) in + connection.send(content: data, completion: .contentProcessed { error in + if let error { + cont.resume(throwing: error) + } else { + cont.resume() + } + }) + } + + Logger.tak.debug("Sent CoT message to client: type=\(cotMessage.type)") + } + + /// Disconnect this client + func disconnect() { + guard isConnected else { return } + + Logger.tak.info("Disconnecting TAK client: \(self.connection.endpoint.debugDescription)") + + isConnected = false + readerTask?.cancel() + readerTask = nil + keepaliveTask?.cancel() + keepaliveTask = nil + connection.cancel() + messageBuffer.removeAll() + + continuation?.yield(.disconnected) + continuation?.finish() + continuation = nil + } +} + +// MARK: - Supporting Types + +/// Information about a connected TAK client +struct TAKClientInfo: Identifiable, Sendable { + let id = UUID() + let endpoint: NWEndpoint + var callsign: String? + var uid: String? + let connectedAt: Date + + init(endpoint: NWEndpoint, callsign: String? = nil, uid: String? = nil, connectedAt: Date = Date()) { + self.endpoint = endpoint + self.callsign = callsign + self.uid = uid + self.connectedAt = connectedAt + } + + var displayName: String { + callsign ?? uid ?? endpoint.debugDescription + } +} + +/// Events emitted by a TAK connection +enum TAKConnectionEvent: Sendable { + case connected(TAKClientInfo) + case clientInfoUpdated(TAKClientInfo) + case message(CoTMessage) + case disconnected + case error(Error) +} + +/// Errors specific to TAK connections +enum TAKConnectionError: LocalizedError { + case connectionClosed + case notConnected + case encodingFailed + case sendFailed(String) + + var errorDescription: String? { + switch self { + case .connectionClosed: + return "Connection was closed" + case .notConnected: + return "Not connected" + case .encodingFailed: + return "Failed to encode CoT message" + case .sendFailed(let reason): + return "Failed to send: \(reason)" + } + } +} diff --git a/Meshtastic/Helpers/TAK/TAKDataPackageGenerator.swift b/Meshtastic/Helpers/TAK/TAKDataPackageGenerator.swift new file mode 100644 index 00000000..427a7aa8 --- /dev/null +++ b/Meshtastic/Helpers/TAK/TAKDataPackageGenerator.swift @@ -0,0 +1,261 @@ +// +// TAKDataPackageGenerator.swift +// Meshtastic +// +// Created by niccellular 12/26/25 +// + +import Foundation +import OSLog + +/// Generates TAK data packages (.zip) for configuring TAK clients like ITAK +/// to connect to the Meshtastic TAK server +final class TAKDataPackageGenerator { + + static let shared = TAKDataPackageGenerator() + + private init() {} + + // MARK: - Data Package Generation + + /// Generate a TAK data package for ITAK client configuration + /// - Parameters: + /// - serverHost: The server hostname/IP (default: 127.0.0.1 for localhost) + /// - port: The server port + /// - useTLS: Whether to use TLS (ssl) with mTLS or plain TCP + /// - description: Description shown in TAK client + /// - Returns: URL to the generated zip file, or nil if generation failed + func generateDataPackage( + serverHost: String = "127.0.0.1", + port: Int, + useTLS: Bool = true, + description: String = "Meshtastic TAK Server" + ) -> URL? { + let fileManager = FileManager.default + + // Create temporary directory for package contents + let packageName = "Meshtastic_TAK_Server" + let tempDir = fileManager.temporaryDirectory.appendingPathComponent(packageName) + + do { + // Clean up any existing temp directory + if fileManager.fileExists(atPath: tempDir.path) { + try fileManager.removeItem(at: tempDir) + } + try fileManager.createDirectory(at: tempDir, withIntermediateDirectories: true) + + // Create certs subdirectory (matches working data package structure) + let certsDir = tempDir.appendingPathComponent("certs") + try fileManager.createDirectory(at: certsDir, withIntermediateDirectories: true) + + // Generate preference file in certs directory + let prefFileName = "meshtastic-server.pref" + let configPref = generateConfigPref( + serverHost: serverHost, + port: port, + useTLS: useTLS, + description: description + ) + let configPrefURL = certsDir.appendingPathComponent(prefFileName) + try configPref.write(to: configPrefURL, atomically: true, encoding: .utf8) + Logger.tak.debug("Created certs/\(prefFileName)") + + // Copy certificates (only needed for TLS/mTLS mode) + if useTLS { + // Truststore (server cert for verifying server) - uses custom if available + if let serverP12Data = TAKCertificateManager.shared.getActiveServerP12Data() { + let truststoreURL = certsDir.appendingPathComponent("truststore.p12") + try serverP12Data.write(to: truststoreURL) + Logger.tak.debug("Created certs/truststore.p12 (custom: \(TAKCertificateManager.shared.hasCustomServerCertificate()))") + } else { + Logger.tak.warning("No server certificate data available") + } + + // Client certificate for mTLS - uses custom if available + if let clientP12Data = TAKCertificateManager.shared.getActiveClientP12Data() { + let clientURL = certsDir.appendingPathComponent("client.p12") + try clientP12Data.write(to: clientURL) + Logger.tak.debug("Created certs/client.p12 (custom: \(TAKCertificateManager.shared.hasCustomClientP12()))") + } else { + Logger.tak.warning("No client certificate data available") + } + } + + // Generate manifest.xml at root level (not in subdirectory) + let manifest = generateManifest(description: description, useTLS: useTLS, prefFileName: prefFileName) + let manifestURL = tempDir.appendingPathComponent("manifest.xml") + try manifest.write(to: manifestURL, atomically: true, encoding: .utf8) + Logger.tak.debug("Created manifest.xml") + + // Create the zip file in Documents directory for better share sheet compatibility + let zipFileName = "\(packageName).zip" + guard let documentsDir = fileManager.urls(for: .documentDirectory, in: .userDomainMask).first else { + Logger.tak.error("Could not get Documents directory") + return nil + } + let zipURL = documentsDir.appendingPathComponent(zipFileName) + + // Remove existing zip if present + if fileManager.fileExists(atPath: zipURL.path) { + try fileManager.removeItem(at: zipURL) + } + + // Create zip archive + try createZipArchive(from: tempDir, to: zipURL) + + // Verify zip was created + guard fileManager.fileExists(atPath: zipURL.path) else { + Logger.tak.error("ZIP file was not created") + return nil + } + + // Cleanup temp directory + try? fileManager.removeItem(at: tempDir) + + Logger.tak.info("Generated TAK data package: \(zipURL.path)") + return zipURL + + } catch { + Logger.tak.error("Failed to generate TAK data package: \(error.localizedDescription)") + try? fileManager.removeItem(at: tempDir) + return nil + } + } + + // MARK: - Pref File Generation (matches working TAK data package format) + + private func generateConfigPref(serverHost: String, port: Int, useTLS: Bool, description: String) -> String { + let protocolType = useTLS ? "ssl" : "tcp" + // Use active certificate passwords (custom if available, otherwise bundled) + let serverPassword = TAKCertificateManager.shared.getActiveServerCertificatePassword() + let clientPassword = TAKCertificateManager.shared.getActiveClientCertificatePassword() + + if useTLS { + // TLS mode with mTLS (mutual TLS with client certificate) + return """ + + + + 1 + \(escapeXML(description)) + true + \(serverHost):\(port):\(protocolType) + + + true + cert/truststore.p12 + \(serverPassword) + cert/client.p12 + \(clientPassword) + + + """ + } else { + // TCP mode - no certificates needed + return """ + + + + 1 + \(escapeXML(description)) + true + \(serverHost):\(port):\(protocolType) + + + true + + + """ + } + } + + // MARK: - Manifest Generation (matches working TAK data package format) + + private func generateManifest(description: String, useTLS: Bool, prefFileName: String) -> String { + let uid = UUID().uuidString + + if useTLS { + // TLS mode with mTLS - includes truststore and client certificate + return """ + + + + + + + + + + + + + """ + } else { + // TCP mode - just the pref file + return """ + + + + + + + + + + + """ + } + } + + // MARK: - Helper Methods + + private func escapeXML(_ string: String) -> String { + return string + .replacingOccurrences(of: "&", with: "&") + .replacingOccurrences(of: "<", with: "<") + .replacingOccurrences(of: ">", with: ">") + .replacingOccurrences(of: "\"", with: """) + .replacingOccurrences(of: "'", with: "'") + } + + // MARK: - ZIP Archive Creation + + /// Create a ZIP archive from a directory + private func createZipArchive(from sourceDir: URL, to destinationURL: URL) throws { + let fileManager = FileManager.default + var copyError: Error? + + // Use NSFileCoordinator to create zip - this is the built-in approach on iOS + var coordinatorError: NSError? + let coordinator = NSFileCoordinator() + + Logger.tak.debug("Creating ZIP from: \(sourceDir.path)") + + coordinator.coordinate( + readingItemAt: sourceDir, + options: .forUploading, + error: &coordinatorError + ) { zipURL in + Logger.tak.debug("Coordinator provided ZIP at: \(zipURL.path)") + do { + // The coordinator creates a temporary zip, copy it to our destination + if fileManager.fileExists(atPath: destinationURL.path) { + try fileManager.removeItem(at: destinationURL) + } + try fileManager.copyItem(at: zipURL, to: destinationURL) + Logger.tak.debug("Copied ZIP to: \(destinationURL.path)") + } catch { + Logger.tak.error("Failed to copy ZIP: \(error.localizedDescription)") + copyError = error + } + } + + if let coordinatorError = coordinatorError { + Logger.tak.error("Coordinator error: \(coordinatorError.localizedDescription)") + throw coordinatorError + } + if let copyError = copyError { + throw copyError + } + } +} diff --git a/Meshtastic/Helpers/TAK/TAKMeshtasticBridge.swift b/Meshtastic/Helpers/TAK/TAKMeshtasticBridge.swift new file mode 100644 index 00000000..9ed42c90 --- /dev/null +++ b/Meshtastic/Helpers/TAK/TAKMeshtasticBridge.swift @@ -0,0 +1,516 @@ +// +// TAKMeshtasticBridge.swift +// Meshtastic +// +// Created by niccellular 12/26/25 +// + +import Foundation +import MeshtasticProtobufs +import OSLog +import CoreData + +/// Bridges CoT messages between TAK clients and the Meshtastic mesh network +/// Handles bidirectional conversion and message routing +@MainActor +final class TAKMeshtasticBridge { + + weak var accessoryManager: AccessoryManager? + weak var takServerManager: TAKServerManager? + + /// Core Data context for node lookups + var context: NSManagedObjectContext? + + /// Lookup table mapping callsigns to device UIDs + /// Populated when receiving PLI packets from other TAK users + /// Key: callsign (e.g., "OLD SALT"), Value: device UID (e.g., "ANDROID-abc123-def456") + private var callsignToDeviceUID: [String: String] = [:] + + init(accessoryManager: AccessoryManager?, takServerManager: TAKServerManager?) { + self.accessoryManager = accessoryManager + self.takServerManager = takServerManager + } + + // MARK: - Callsign to Device UID Mapping + + /// Register a callsign → device UID mapping (called when receiving PLI from other users) + func registerContact(callsign: String, deviceUID: String) { + guard !callsign.isEmpty, !deviceUID.isEmpty else { return } + // Extract actual device UID in case it has a smuggled messageId + let (actualDeviceUID, _) = Self.parseDeviceCallsign(deviceUID) + guard !actualDeviceUID.isEmpty else { return } + let previousUID = callsignToDeviceUID[callsign] + callsignToDeviceUID[callsign] = actualDeviceUID + if previousUID != actualDeviceUID { + Logger.tak.debug("Registered contact: \(callsign) → \(actualDeviceUID)") + } + } + + // MARK: - Read Receipt Handling + + /// Receipt type for GeoChat read receipts + enum ReceiptType { + case delivered // ACK:D - Message delivered to device + case read // ACK:R - Message read by user + } + + /// Parsed read receipt from a GeoChat message + struct ParsedReceipt { + let type: ReceiptType + let messageId: String + } + + /// Check if a GeoChat message is a read receipt + /// Receipt format: "ACK:D:" or "ACK:R:" + /// - Parameter message: The GeoChat message content + /// - Returns: Parsed receipt if this is a receipt, nil otherwise + nonisolated static func parseReceipt(from message: String) -> ParsedReceipt? { + guard message.hasPrefix("ACK:") else { return nil } + + let parts = message.split(separator: ":", maxSplits: 2) + guard parts.count == 3 else { + return nil + } + + let receiptTypeString = String(parts[1]) + let messageId = String(parts[2]) + + guard !messageId.isEmpty else { return nil } + + let receiptType: ReceiptType + switch receiptTypeString { + case "D": + receiptType = .delivered + case "R": + receiptType = .read + default: + return nil + } + + return ParsedReceipt(type: receiptType, messageId: messageId) + } + + /// Check if a TAKPacket GeoChat is a read receipt + nonisolated static func isReceipt(_ takPacket: TAKPacket) -> Bool { + guard case .chat(let geoChat) = takPacket.payloadVariant else { + return false + } + return geoChat.message.hasPrefix("ACK:") + } + + // MARK: - MessageId Smuggling in device_callsign + + /// Parse a device_callsign that may contain a smuggled messageId + /// Format: "|" or just "" + /// - Parameter combined: The device_callsign field value + /// - Returns: Tuple of (actualDeviceCallsign, messageId) where messageId is nil if not present + nonisolated static func parseDeviceCallsign(_ combined: String?) -> (deviceCallsign: String, messageId: String?) { + guard let combined = combined, !combined.isEmpty else { + return ("", nil) + } + + if let separatorIndex = combined.firstIndex(of: "|") { + let deviceCallsign = String(combined[..|" + /// - Parameters: + /// - deviceCallsign: The actual device UID + /// - messageId: The message ID to smuggle + /// - Returns: Combined string with messageId appended + nonisolated static func createSmuggledDeviceCallsign(deviceCallsign: String, messageId: String) -> String { + return "\(deviceCallsign)|\(messageId)" + } + + /// Look up a device UID from a callsign + func lookupDeviceUID(forCallsign callsign: String) -> String? { + return callsignToDeviceUID[callsign] + } + + // MARK: - TAK → Meshtastic (CoT to TAKPacket) + + /// Send a CoT message received from TAK to the Meshtastic mesh + func sendToMesh(_ cotMessage: CoTMessage) async { + guard let accessoryManager else { + Logger.tak.warning("Cannot send to mesh: AccessoryManager not available") + return + } + + guard accessoryManager.isConnected else { + Logger.tak.warning("Cannot send to mesh: Not connected to Meshtastic device") + return + } + + // Determine send method based on CoT type + let sendMethod = GenericCoTHandler.shared.classifySendMethod(for: cotMessage) + + switch sendMethod { + case .takPacketPLI, .takPacketChat: + // Use TAKPacket protobuf on ATAK_PLUGIN port (72) + guard let takPacket = convertToTAKPacket(cot: cotMessage) else { + Logger.tak.warning("Failed to convert CoT to TAKPacket: \(cotMessage.type)") + return + } + + do { + try await accessoryManager.sendTAKPacket(takPacket) + Logger.tak.info("Sent TAKPacket to mesh: \(cotMessage.type)") + } catch { + Logger.tak.error("Failed to send TAKPacket to mesh: \(error.localizedDescription)") + } + + case .exiDirect, .exiFountain: + // Use EXI compression on ATAK_FORWARDER port (257) + GenericCoTHandler.shared.accessoryManager = accessoryManager + do { + try await GenericCoTHandler.shared.sendGenericCoT(cotMessage) + Logger.tak.info("Sent generic CoT to mesh via ATAK_FORWARDER: \(cotMessage.type)") + } catch { + Logger.tak.error("Failed to send generic CoT to mesh: \(error.localizedDescription)") + } + } + } + + /// Convert CoT message to Meshtastic TAKPacket protobuf + func convertToTAKPacket(cot: CoTMessage) -> TAKPacket? { + Logger.tak.debug("=== CoT → TAKPacket Conversion ===") + Logger.tak.debug("CoT Input:") + Logger.tak.debug(" uid: \(cot.uid)") + Logger.tak.debug(" type: \(cot.type)") + Logger.tak.debug(" lat: \(cot.latitude), lon: \(cot.longitude), hae: \(cot.hae)") + Logger.tak.debug(" contact: \(cot.contact?.callsign ?? "nil")") + Logger.tak.debug(" group: \(cot.group?.name ?? "nil") / \(cot.group?.role ?? "nil")") + Logger.tak.debug(" status.battery: \(cot.status?.battery ?? -1)") + Logger.tak.debug(" track: speed=\(cot.track?.speed ?? -1), course=\(cot.track?.course ?? -1)") + Logger.tak.debug(" chat: \(cot.chat?.message ?? "nil")") + Logger.tak.debug(" remarks: \(cot.remarks ?? "nil")") + + var takPacket = TAKPacket() + + // Contact information + if let contact = cot.contact { + var cotContact = Contact() + cotContact.callsign = contact.callsign + cotContact.deviceCallsign = cot.uid + takPacket.contact = cotContact + Logger.tak.debug("TAKPacket.contact: callsign=\(cotContact.callsign), deviceCallsign=\(cotContact.deviceCallsign)") + } + + // Group/Team information + if let group = cot.group { + var cotGroup = Group() + cotGroup.team = Team.fromColorName(group.name) + cotGroup.role = MemberRole.fromRoleName(group.role) + takPacket.group = cotGroup + Logger.tak.debug("TAKPacket.group: team=\(cotGroup.team.rawValue), role=\(cotGroup.role.rawValue)") + } + + // Status (battery) + if let status = cot.status { + var cotStatus = Status() + cotStatus.battery = UInt32(max(0, status.battery)) + takPacket.status = cotStatus + Logger.tak.debug("TAKPacket.status: battery=\(cotStatus.battery)") + } + + // Determine payload type based on CoT type + // Accept any friendly ground unit type (a-f-G...) for PLI + if cot.type.hasPrefix("a-f-G") || cot.type.hasPrefix("a-f-g") { + // Register this TAK client's contact info for future DM lookups + if let contact = cot.contact, !contact.callsign.isEmpty, !cot.uid.isEmpty { + registerContact(callsign: contact.callsign, deviceUID: cot.uid) + } + + // Atom type (position) - create PLI + var pli = PLI() + + // Convert lat/lon to integer format (degrees * 1e7) + let latI = Int32(cot.latitude * 1e7) + let lonI = Int32(cot.longitude * 1e7) + + // Handle altitude - clamp to valid Int32 range, use 0 for unknown (9999999) + let altitudeValue: Int32 + if cot.hae >= 9999999.0 || cot.hae.isNaN || cot.hae.isInfinite { + altitudeValue = 0 // Unknown altitude + } else { + altitudeValue = Int32(clamping: Int(cot.hae)) + } + + pli.latitudeI = latI + pli.longitudeI = lonI + pli.altitude = altitudeValue + + if let track = cot.track { + pli.speed = UInt32(max(0, track.speed)) + pli.course = UInt32(max(0, track.course)) + } + + takPacket.pli = pli + + Logger.tak.debug("TAKPacket.pli created:") + Logger.tak.debug(" latitudeI: \(pli.latitudeI) (from \(cot.latitude))") + Logger.tak.debug(" longitudeI: \(pli.longitudeI) (from \(cot.longitude))") + Logger.tak.debug(" altitude: \(pli.altitude) (from \(cot.hae))") + Logger.tak.debug(" speed: \(pli.speed), course: \(pli.course)") + + } else if cot.type == "b-t-f" { + // Chat message - MUST include contact field for sender identification + var geoChat = GeoChat() + + // Extract messageId from CoT uid if present + // CoT uid format: "GeoChat.{senderUid}.{chatroom}.{messageId}" + var messageId: String? + var actualDeviceUid = cot.uid + let uidComponents = cot.uid.components(separatedBy: ".") + if uidComponents.count >= 4 && uidComponents[0] == "GeoChat" { + // Extract the actual device UID (second component) + actualDeviceUid = uidComponents[1] + // Extract messageId (last component) + messageId = uidComponents.last + Logger.tak.debug("GeoChat: Extracted messageId=\(messageId ?? "nil") from uid") + } + + // If no messageId found, generate one + if messageId == nil || messageId?.isEmpty == true { + messageId = UUID().uuidString + Logger.tak.debug("GeoChat: Generated new messageId=\(messageId!)") + } + + // Ensure contact (sender info) is always set for chat messages + // This is REQUIRED for Android ATAK to process the message correctly + if !takPacket.hasContact { + var senderContact = Contact() + // Get sender callsign from chat.senderCallsign or cot.contact + if let senderCallsign = cot.chat?.senderCallsign, !senderCallsign.isEmpty { + senderContact.callsign = senderCallsign + } else if let contactCallsign = cot.contact?.callsign, !contactCallsign.isEmpty { + senderContact.callsign = contactCallsign + } else { + senderContact.callsign = "Unknown" + } + // Smuggle messageId into device_callsign for proper threading on Android + // Format: "|" + senderContact.deviceCallsign = Self.createSmuggledDeviceCallsign( + deviceCallsign: actualDeviceUid, + messageId: messageId! + ) + takPacket.contact = senderContact + Logger.tak.debug("GeoChat: Added sender contact - callsign=\(senderContact.callsign), smuggled deviceCallsign=\(senderContact.deviceCallsign)") + } else { + // Contact already set, but we still need to smuggle the messageId + var updatedContact = takPacket.contact + let existingDeviceCallsign = updatedContact.deviceCallsign.isEmpty ? actualDeviceUid : updatedContact.deviceCallsign + updatedContact.deviceCallsign = Self.createSmuggledDeviceCallsign( + deviceCallsign: existingDeviceCallsign, + messageId: messageId! + ) + takPacket.contact = updatedContact + Logger.tak.debug("GeoChat: Updated contact with smuggled messageId - deviceCallsign=\(updatedContact.deviceCallsign)") + } + + if let chat = cot.chat { + geoChat.message = chat.message + + // Handle recipient addressing + // chat.chatroom contains either "All Chat Rooms" or the recipient's callsign + if chat.chatroom == "All Chat Rooms" { + // Broadcast message - set to literal "All Chat Rooms" + geoChat.to = "All Chat Rooms" + Logger.tak.debug("GeoChat: Broadcast to All Chat Rooms") + } else { + // Direct message - need to look up recipient's device UID from their callsign + let recipientCallsign = chat.chatroom + if let recipientDeviceUID = lookupDeviceUID(forCallsign: recipientCallsign) { + // Found the recipient's device UID + geoChat.to = recipientDeviceUID + geoChat.toCallsign = recipientCallsign + Logger.tak.debug("GeoChat DM: to=\(recipientDeviceUID), toCallsign=\(recipientCallsign)") + } else { + // Recipient device UID not found - use callsign as fallback + // This may not work on Android but is better than nothing + geoChat.to = recipientCallsign + geoChat.toCallsign = recipientCallsign + Logger.tak.warning("GeoChat DM: Unknown device UID for '\(recipientCallsign)', using callsign as fallback") + } + } + } else if let remarks = cot.remarks { + geoChat.message = remarks + geoChat.to = "All Chat Rooms" + } + + takPacket.chat = geoChat + + Logger.tak.debug("TAKPacket.chat created:") + Logger.tak.debug(" message: \(geoChat.message)") + Logger.tak.debug(" to: \(geoChat.to)") + Logger.tak.debug(" toCallsign: \(geoChat.toCallsign)") + Logger.tak.debug(" sender.callsign: \(takPacket.contact.callsign)") + Logger.tak.debug(" sender.deviceCallsign: \(takPacket.contact.deviceCallsign)") + + } else { + // Unknown type, skip + Logger.tak.debug("Skipping CoT type not mapped to TAKPacket: \(cot.type)") + return nil + } + + // Log the final TAKPacket structure + Logger.tak.debug("TAKPacket output:") + Logger.tak.debug(" hasContact: \(takPacket.hasContact)") + Logger.tak.debug(" hasGroup: \(takPacket.hasGroup)") + Logger.tak.debug(" hasStatus: \(takPacket.hasStatus)") + Logger.tak.debug(" payloadVariant: \(String(describing: takPacket.payloadVariant))") + + // Log serialized size for debugging + do { + let serialized = try takPacket.serializedData() + Logger.tak.debug(" serializedSize: \(serialized.count) bytes") + Logger.tak.debug(" serializedHex: \(serialized.prefix(64).map { String(format: "%02x", $0) }.joined(separator: " "))\(serialized.count > 64 ? "..." : "")") + } catch { + Logger.tak.error(" Failed to serialize TAKPacket: \(error.localizedDescription)") + } + + Logger.tak.debug("=== End Conversion ===") + return takPacket + } + + // MARK: - Meshtastic → TAK (TAKPacket to CoT) + + /// Broadcast a Meshtastic TAKPacket to all connected TAK clients + func broadcastToTAKClients(_ takPacket: TAKPacket, from nodeNum: UInt32) async { + // Register contact info from incoming TAKPackets (for callsign → deviceUID lookup) + if takPacket.hasContact { + let callsign = takPacket.contact.callsign + let deviceUID = takPacket.contact.deviceCallsign + if !callsign.isEmpty && !deviceUID.isEmpty { + registerContact(callsign: callsign, deviceUID: deviceUID) + } + } + + // Check if this is a read receipt - don't forward to TAK clients as chat message + if case .chat(let geoChat) = takPacket.payloadVariant { + if let receipt = Self.parseReceipt(from: geoChat.message) { + // This is a read receipt, handle it internally + let typeString = receipt.type == .delivered ? "Delivered" : "Read" + Logger.tak.info("Received \(typeString) receipt for messageId: \(receipt.messageId) from node \(nodeNum)") + // TODO: Update message status in Core Data if we track sent messages + // For now, just log and don't forward to TAK clients + return + } + } + + guard let takServerManager else { + Logger.tak.debug("Cannot broadcast to TAK: TAKServerManager not available") + return + } + + guard takServerManager.isRunning else { + Logger.tak.debug("Cannot broadcast to TAK: Server not running") + return + } + + guard !takServerManager.connectedClients.isEmpty else { + Logger.tak.debug("No TAK clients connected, skipping broadcast") + return + } + + // Look up node info for additional context + let nodeInfo = lookupNodeInfo(nodeNum: nodeNum) + + // Convert to CoT + guard let cotMessage = convertToCoT(from: takPacket, nodeNum: nodeNum, nodeInfo: nodeInfo) else { + Logger.tak.warning("Failed to convert TAKPacket to CoT from node \(nodeNum)") + return + } + + // Broadcast to all TAK clients + await takServerManager.broadcast(cotMessage) + Logger.tak.info("Broadcast CoT to TAK clients: \(cotMessage.type) from node \(nodeNum)") + } + + /// Convert Meshtastic TAKPacket to CoT message + func convertToCoT(from takPacket: TAKPacket, nodeNum: UInt32, nodeInfo: NodeInfoEntity?) -> CoTMessage? { + // Use the factory method from CoTMessage which handles the conversion + let deviceUid = "MESHTASTIC-\(String(format: "%08X", nodeNum))" + return CoTMessage.fromTAKPacket(takPacket, deviceUid: deviceUid) + } + + /// Create a CoT PLI message from a Meshtastic node's position + func createCoTFromNode(_ node: NodeInfoEntity) -> CoTMessage? { + guard let position = node.latestPosition, + let latitude = position.latitude, + let longitude = position.longitude, + latitude != 0 || longitude != 0 else { + return nil + } + + let uid = "MESHTASTIC-\(String(format: "%08X", node.num))" + let callsign = node.user?.shortName ?? node.user?.longName ?? "MESH-\(node.num)" + + // Get battery level from device metrics + let battery = Int(node.latestDeviceMetrics?.batteryLevel ?? 100) + + return CoTMessage.pli( + uid: uid, + callsign: callsign, + latitude: latitude, + longitude: longitude, + altitude: Double(position.altitude), + speed: Double(position.speed), + course: Double(position.heading), + team: "Green", // Meshtastic nodes shown as green by default + role: "Team Member", + battery: battery, + staleMinutes: 15 // Meshtastic positions can be older + ) + } + + // MARK: - Broadcast All Mesh Nodes to TAK + + /// Send all known mesh node positions to TAK clients + /// Useful when a new TAK client connects + func broadcastAllNodesToTAK() async { + guard let takServerManager, takServerManager.isRunning else { return } + guard let context else { return } + + let fetchRequest: NSFetchRequest = NodeInfoEntity.fetchRequest() + // Only nodes with valid positions + fetchRequest.predicate = NSPredicate(format: "latestPosition != nil") + + do { + let nodes = try context.fetch(fetchRequest) + + for node in nodes { + if let cotMessage = createCoTFromNode(node) { + await takServerManager.broadcast(cotMessage) + } + } + + Logger.tak.info("Broadcast \(nodes.count) mesh node positions to TAK clients") + } catch { + Logger.tak.error("Failed to fetch nodes for TAK broadcast: \(error.localizedDescription)") + } + } + + // MARK: - Helper Methods + + private func lookupNodeInfo(nodeNum: UInt32) -> NodeInfoEntity? { + guard let context else { return nil } + + let fetchRequest: NSFetchRequest = NodeInfoEntity.fetchRequest() + fetchRequest.predicate = NSPredicate(format: "num == %d", Int64(nodeNum)) + fetchRequest.fetchLimit = 1 + + do { + return try context.fetch(fetchRequest).first + } catch { + Logger.tak.warning("Failed to lookup node info for \(nodeNum): \(error.localizedDescription)") + return nil + } + } +} diff --git a/Meshtastic/Helpers/TAK/TAKServerManager.swift b/Meshtastic/Helpers/TAK/TAKServerManager.swift new file mode 100644 index 00000000..b71fa848 --- /dev/null +++ b/Meshtastic/Helpers/TAK/TAKServerManager.swift @@ -0,0 +1,427 @@ +// +// TAKServerManager.swift +// Meshtastic +// +// Created by niccellular 12/26/25 +// + +import Foundation +import Network +import OSLog +import Combine +import SwiftUI + +/// Manages the TAK Server lifecycle, TLS connections, and client management +/// Runs on MainActor for thread safety, following the AccessoryManager pattern +@MainActor +final class TAKServerManager: ObservableObject { + + static let shared = TAKServerManager() + + // MARK: - Published State + + @Published private(set) var isRunning = false + @Published private(set) var connectedClients: [TAKClientInfo] = [] + @Published var lastError: String? + + // MARK: - Configuration (persisted via AppStorage) + + @AppStorage("takServerEnabled") var enabled = false { + didSet { + Task { + if enabled && !isRunning { + try? await start() + } else if !enabled && isRunning { + stop() + } + } + } + } + + /// Fixed port - always use TLS port 8089 + static let defaultTLSPort = 8089 + static let defaultTCPPort = 8087 // Legacy, not used + + /// Port is fixed to 8089 (mTLS) + var port: Int { Self.defaultTLSPort } + + /// Always use TLS/mTLS + var useTLS: Bool { true } + + // MARK: - Bridge + + /// Bridge for converting between CoT and Meshtastic formats + var bridge: TAKMeshtasticBridge? + + // MARK: - Private Properties + + private var listener: NWListener? + private var connections: [ObjectIdentifier: TAKConnection] = [:] + private var connectionTasks: [ObjectIdentifier: Task] = [:] + private let queue = DispatchQueue(label: "tak.server", qos: .userInitiated) + + private init() {} + + // MARK: - Initialization + + /// Initialize the TAK server on app startup + /// Call this from app initialization to restore server state + func initializeOnStartup() { + guard enabled else { + Logger.tak.debug("TAK Server not enabled, skipping startup") + return + } + + guard !isRunning else { + Logger.tak.debug("TAK Server already running") + return + } + + Logger.tak.info("TAK Server enabled, starting on app launch") + Task { + do { + try await start() + } catch { + Logger.tak.error("Failed to start TAK Server on startup: \(error.localizedDescription)") + } + } + } + + // MARK: - Server Lifecycle + + /// Start the TAK server (TLS or TCP based on configuration) + func start() async throws { + guard !isRunning else { + Logger.tak.info("TAK Server already running") + return + } + + let mode = useTLS ? "TLS" : "TCP" + Logger.tak.info("Starting TAK Server on port \(self.port) (\(mode) mode)") + + let parameters: NWParameters + + if useTLS { + // Validate we have a server certificate for TLS mode + guard let identity = TAKCertificateManager.shared.getServerIdentity() else { + let error = TAKServerError.noServerCertificate + lastError = error.localizedDescription + enabled = false + throw error + } + + // Create TLS options + let tlsOptions = NWProtocolTLS.Options() + + // Set server identity (certificate + private key) + let secIdentity = sec_identity_create(identity)! + sec_protocol_options_set_local_identity( + tlsOptions.securityProtocolOptions, + secIdentity + ) + + // Set minimum TLS version to 1.2 (TAK standard) + sec_protocol_options_set_min_tls_protocol_version( + tlsOptions.securityProtocolOptions, + .TLSv12 + ) + + // Configure mTLS - always require client certificate for TLS mode + sec_protocol_options_set_peer_authentication_required( + tlsOptions.securityProtocolOptions, + true + ) + + // Set up client certificate validation + let clientCAs = TAKCertificateManager.shared.getClientCACertificates() + Logger.tak.info("Loaded \(clientCAs.count) CA certificate(s) for client validation") + if !clientCAs.isEmpty { + for (index, ca) in clientCAs.enumerated() { + if let summary = SecCertificateCopySubjectSummary(ca) as String? { + Logger.tak.info("CA[\(index)]: \(summary)") + } + } + let trustRoots = clientCAs as CFArray + sec_protocol_options_set_verify_block( + tlsOptions.securityProtocolOptions, + { _, secTrust, completion in + // Convert sec_trust_t to SecTrust + let trust = sec_trust_copy_ref(secTrust).takeRetainedValue() + + // Set policy for client certificate validation + // Use SSL policy with server=false to validate client certificates + // This properly accepts clientAuth ExtendedKeyUsage + let clientPolicy = SecPolicyCreateSSL(false, nil) + SecTrustSetPolicies(trust, clientPolicy) + + SecTrustSetAnchorCertificates(trust, trustRoots) + SecTrustSetAnchorCertificatesOnly(trust, true) + var error: CFError? + let isValid = SecTrustEvaluateWithError(trust, &error) + if let error = error { + Logger.tak.error("Client cert validation error: \(error.localizedDescription)") + } + Logger.tak.info("Client certificate validation: \(isValid ? "passed" : "failed")") + completion(isValid) + }, + queue + ) + } else { + Logger.tak.warning("mTLS enabled but no CA certificates configured for client validation") + } + + // TCP options + let tcpOptions = NWProtocolTCP.Options() + tcpOptions.enableKeepalive = true + tcpOptions.keepaliveIdle = 60 + + parameters = NWParameters(tls: tlsOptions, tcp: tcpOptions) + } else { + // Plain TCP mode (no TLS) + let tcpOptions = NWProtocolTCP.Options() + tcpOptions.enableKeepalive = true + tcpOptions.keepaliveIdle = 60 + + parameters = NWParameters(tls: nil, tcp: tcpOptions) + } + + parameters.allowLocalEndpointReuse = true + + // Bind to localhost only - only allow TAK clients on the same device + parameters.requiredLocalEndpoint = NWEndpoint.hostPort( + host: NWEndpoint.Host("127.0.0.1"), + port: NWEndpoint.Port(integerLiteral: UInt16(port)) + ) + + // Create and configure listener + do { + listener = try NWListener(using: parameters) + } catch { + lastError = "Failed to create listener: \(error.localizedDescription)" + Logger.tak.error("Failed to create TAK listener: \(error.localizedDescription)") + enabled = false + throw error + } + + // Set up state handler + listener?.stateUpdateHandler = { [weak self] state in + Task { @MainActor in + self?.handleListenerState(state) + } + } + + // Set up new connection handler + listener?.newConnectionHandler = { [weak self] connection in + Task { @MainActor in + await self?.handleNewConnection(connection) + } + } + + // Start listening + listener?.start(queue: queue) + } + + /// Stop the TAK server + func stop() { + Logger.tak.info("Stopping TAK Server") + + listener?.cancel() + listener = nil + + // Cancel all connection tasks + for (_, task) in connectionTasks { + task.cancel() + } + connectionTasks.removeAll() + + // Disconnect all clients + for (_, connection) in connections { + Task { + await connection.disconnect() + } + } + connections.removeAll() + connectedClients.removeAll() + + isRunning = false + lastError = nil + + Logger.tak.info("TAK Server stopped") + } + + /// Restart the server (useful after configuration changes) + func restart() async throws { + stop() + try await Task.sleep(nanoseconds: 500_000_000) // 0.5s delay + try await start() + } + + // MARK: - State Handling + + private func handleListenerState(_ state: NWListener.State) { + switch state { + case .ready: + isRunning = true + lastError = nil + Logger.tak.info("TAK Server listening on port \(self.port)") + + case .failed(let error): + isRunning = false + lastError = error.localizedDescription + enabled = false + Logger.tak.error("TAK Server failed: \(error.localizedDescription)") + + case .cancelled: + isRunning = false + Logger.tak.info("TAK Server cancelled") + + case .waiting(let error): + Logger.tak.warning("TAK Server waiting: \(error.localizedDescription)") + + case .setup: + Logger.tak.debug("TAK Server setup") + + @unknown default: + break + } + } + + // MARK: - Connection Management + + private func handleNewConnection(_ nwConnection: NWConnection) async { + let connectionId = ObjectIdentifier(nwConnection) + let connection = TAKConnection(connection: nwConnection) + + connections[connectionId] = connection + + Logger.tak.info("New TAK client connecting: \(nwConnection.endpoint.debugDescription)") + + // Start handling the connection + let eventStream = await connection.start() + + // Create task to handle connection events + let task = Task { + for await event in eventStream { + await handleConnectionEvent(event, connectionId: connectionId) + } + // Connection ended + await removeConnection(connectionId) + } + + connectionTasks[connectionId] = task + } + + private func handleConnectionEvent(_ event: TAKConnectionEvent, connectionId: ObjectIdentifier) async { + switch event { + case .connected(let clientInfo): + connectedClients.append(clientInfo) + Logger.tak.info("TAK client connected: \(clientInfo.displayName)") + + case .clientInfoUpdated(let clientInfo): + // Update the client info in our list + if let index = connectedClients.firstIndex(where: { $0.id == clientInfo.id }) { + connectedClients[index] = clientInfo + } + + case .message(let cotMessage): + Logger.tak.info("Received CoT from TAK client: \(cotMessage.type)") + // Forward to Meshtastic mesh via bridge + await bridge?.sendToMesh(cotMessage) + + case .disconnected: + await removeConnection(connectionId) + + case .error(let error): + Logger.tak.error("TAK client error: \(error.localizedDescription)") + } + } + + private func removeConnection(_ connectionId: ObjectIdentifier) async { + connectionTasks[connectionId]?.cancel() + connectionTasks.removeValue(forKey: connectionId) + + if let connection = connections.removeValue(forKey: connectionId) { + let endpoint = await connection.endpoint + connectedClients.removeAll { $0.endpoint.debugDescription == endpoint.debugDescription } + Logger.tak.info("TAK client disconnected") + } + } + + // MARK: - Message Distribution + + /// Broadcast a CoT message to all connected TAK clients + func broadcast(_ cotMessage: CoTMessage) async { + guard !connections.isEmpty else { return } + + Logger.tak.info("Broadcasting CoT to \(self.connections.count) TAK client(s): \(cotMessage.type)") + + for (connectionId, connection) in connections { + do { + try await connection.send(cotMessage) + } catch { + Logger.tak.error("Failed to send to TAK client: \(error.localizedDescription)") + // Remove failed connection + await removeConnection(connectionId) + } + } + } + + /// Send a CoT message to a specific client + func send(_ cotMessage: CoTMessage, to clientId: UUID) async throws { + guard let clientInfo = connectedClients.first(where: { $0.id == clientId }) else { + throw TAKServerError.clientNotFound + } + + for (_, connection) in connections { + let endpoint = await connection.endpoint + if endpoint.debugDescription == clientInfo.endpoint.debugDescription { + try await connection.send(cotMessage) + return + } + } + + throw TAKServerError.clientNotFound + } + + // MARK: - Status + + /// Get server status description + var statusDescription: String { + if isRunning { + let mode = useTLS ? "TLS" : "TCP" + return "Running on port \(port) (\(mode))" + } else if let error = lastError { + return "Error: \(error)" + } else { + return "Stopped" + } + } +} + +// MARK: - Server Errors + +enum TAKServerError: LocalizedError { + case noServerCertificate + case noClientCACertificate + case tlsConfigurationFailed + case listenerFailed(String) + case clientNotFound + case notRunning + + var errorDescription: String? { + switch self { + case .noServerCertificate: + return "No server certificate configured. Import a .p12 file with the server certificate and private key." + case .noClientCACertificate: + return "No client CA certificate configured. Import the CA certificate (.pem) used to sign client certificates." + case .tlsConfigurationFailed: + return "Failed to configure TLS settings." + case .listenerFailed(let reason): + return "Failed to start listener: \(reason)" + case .clientNotFound: + return "Client not found" + case .notRunning: + return "TAK Server is not running" + } + } +} diff --git a/Meshtastic/Meshtastic.entitlements b/Meshtastic/Meshtastic.entitlements index 4dbdb836..e8c10bea 100644 --- a/Meshtastic/Meshtastic.entitlements +++ b/Meshtastic/Meshtastic.entitlements @@ -25,6 +25,8 @@ com.apple.security.network.client + com.apple.security.network.server + com.apple.security.personal-information.location keychain-access-groups diff --git a/Meshtastic/MeshtasticAppDelegate.swift b/Meshtastic/MeshtasticAppDelegate.swift index 19521601..2658a4bf 100644 --- a/Meshtastic/MeshtasticAppDelegate.swift +++ b/Meshtastic/MeshtasticAppDelegate.swift @@ -25,6 +25,10 @@ class MeshtasticAppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificat if locationsHandler.backgroundActivity { locationsHandler.backgroundActivity = true } + // Initialize TAK Server if enabled + Task { @MainActor in + TAKServerManager.shared.initializeOnStartup() + } return true } // Lets us show the notification in the app in the foreground diff --git a/Meshtastic/Resources/Certificates/backup/ca.pem b/Meshtastic/Resources/Certificates/backup/ca.pem new file mode 100644 index 00000000..f00e8c1a --- /dev/null +++ b/Meshtastic/Resources/Certificates/backup/ca.pem @@ -0,0 +1,20 @@ +-----BEGIN CERTIFICATE----- +MIIDQzCCAiugAwIBAgIUZaXYUGEFhPeOcWWNXlwt5qyfIVgwDQYJKoZIhvcNAQEL +BQAwMTEaMBgGA1UEAwwRTWVzaHRhc3RpYyBUQUsgQ0ExEzARBgNVBAoMCk1lc2h0 +YXN0aWMwHhcNMjUxMjI3MjIyNDQ3WhcNMzUxMjI1MjIyNDQ3WjAxMRowGAYDVQQD +DBFNZXNodGFzdGljIFRBSyBDQTETMBEGA1UECgwKTWVzaHRhc3RpYzCCASIwDQYJ +KoZIhvcNAQEBBQADggEPADCCAQoCggEBAL4kQSbJ3eqZg3DGAyD8XPMoeKS2ERy6 +i1w6Uyr70mE5cJoaUISlA+jYo+hKk2ysjct3byuB43XlZBeK0tUTt2900o3/EJXZ +ggRe0yIWrsiMqweRGf3TSgeusz6TrtmZ5KptYaLsc39/MGGKj2v00J+HmFSgDTRu +v5LY8do0haP+XaP5MxWgPcY0ySEB0yEYr7MtOOd6npZaHRJlw8UWALrvHznl7Yrv +80wYo3zBbQ8SeCamCOj+Is/Eye9fixosZi3UkR8FEMUONWtofTI83DfFfP1kDVaq +lWr2fzdlCebK7wY0pY0cBEbdpQadXFQ2PiqXDd3g7k6i+mjT7XzH/mMCAwEAAaNT +MFEwHQYDVR0OBBYEFF/jLHK/wvsWMW8TAbQMIV5BPSxUMB8GA1UdIwQYMBaAFF/j +LHK/wvsWMW8TAbQMIV5BPSxUMA8GA1UdEwEB/wQFMAMBAf8wDQYJKoZIhvcNAQEL +BQADggEBAAAzYYf5ktEHxDRvAd4pf8fv1dgGpuWfdE23h5Tg4wE+0pXCvtXqKGmQ +mPiEr7hAFphSppJZdRl7bvdv+jzllqeCoHgEyUJvJvgMugfG8f8IhIKkg7Q7cd38 +LQrVimjH+g1UKK1/XmJpv7wyDo53wvBsKRxsIwwEPdM4TUkjNIfkgNY5YpnOBrrx +Ubj9T8ZdHc/tM+Z03bgotIejXqh1PbK+Cfq5kXfv37uscOJHBCq8anA1AXsSGS31 +R1IN9vXmQ6kItJErPSJyY1l0PSgniWhYCbxmRmsSIFYlZjVq0BvDQi1Va1W/9LiV +Vp2YyFUrzlbnng24dpvQiSJU+pl/9Lk= +-----END CERTIFICATE----- diff --git a/Meshtastic/Resources/Certificates/backup/server.p12 b/Meshtastic/Resources/Certificates/backup/server.p12 new file mode 100644 index 0000000000000000000000000000000000000000..b7455489438b51bf9f81a095c7b16a2addb4b30d GIT binary patch literal 3459 zcmai%XEYlO`^F_i%-S=gwMWsIt@g2JwN{N5wMuJiY(=(shTCFeo7#BATpLJ+5{Ip!Bi2p>1(f!Sebs?d7#`HjOrNQc$?~#U zy^`sM2ZVnI+RWlAc7_6Tl3byAQ*KXk`R5-8HL7N#8E@9B*Iwv7nCD`ovQ^I!r(`ht;o5QVS;h`N zNs&W4)nIX@)%bq2eAj}zd8`wvHtAmy)DRuDt6*-5my>)Dbn@4lN881DV7TU~ZglRc4rqM%S+^TnUL6juaE)6s8a%tJh&-F>Pfwfk z2o8`dt<~#k6P-5frS6nOZ{PkUQYu5VE|!1PQ^MfU8H4B$jJufU@pDx_&LcCu(T1~t zX71%vdJW=kT>scY%hX4~Q>sYxOc0^hw9i7KkA|PVJRu?4u6^=aE5PK9866!$gJH$^ zxa7maHY-rbg`-@~L10Wz!@IExLVzPjCYoZ?82cDQ_=-`nrHN|Rd}liuG`9HV##k!L zlyrE9k>hk_?<_SBW%2G6U7eNqnmzc&<&Y`fVdJL2cV|-WauUDyv4dt@EyQ>KFa+C{ zUuHL=s9`L~zLT~x*FMhnIDJfqF;tWnUkCP&prt7&wFCi@^6*39Y>zk9@VPcdo9(w6 z@xgM?u$^atLkbUBU|#bLD+{=%3#yC)7hCKqZ^gD==B?s%x5OPycA9P7d8Nhqlpdu} zu%>^KUV+j~iqIVnfO)T;t0&*lK@djVrv$|DJ7I!Q{q2qGE8+&2}SPHO14KfHMrp`JJPjSvG-v zWtW7IdM(keS4#qH@eh<+(x#F%-fAj<$Q&2gBr*3ysyyi zL9)BhHnZA7xPVxXt>Df;wbE)I{_T{;CU(uOZr!|wW_Tl`W%j{aZm#iWPl&c0k|#l< zxrii&8H#J&04h~ZTK)vL^Sn2Ytb3gq@T#}@DXt-?9pjE}DR{8WN^9R8MGfI%s1^GZ zi!hCBbs#L<`|7+XE8oJYYD~sy{3>=0Ddm&E5uk{T__*CQk@$k?w{AK8VYExtCo|)lSJXo* zK<1y3jkys%VvbQTFU{aE8GEN zXR?8~Onb+}(BEP1_w!4p#N5t|^0u%)fIa%sFwIe&`qu;F@=2ca&@In*AhJpsv1PwG zM)=UpiMC)p3y*UGUYSdp@@DPVqHFH~z3D{sxrYUbZC8UpLQoN*gD!*i8y!ux3VcNC;Q2wGBM^FTx|4}CB_DYqL@KWM@fplgTW!oNRnfE2 zr@z0Myn|J=3QgOPPo6*TW*C>&a&k#LbmHtqOpUc4_T^gczH=rB7y9e4?;q{4)I_&3 zcgvuz;q!g&p+4BgyuoB2(G4KfPgx)B%M8}Qx?0*G8e zH$F3Qdj#{dHouN@3flN1&V`3X$h`JAC*DZKt$?CgJ>^Tpkm;giamX_MM{a{vf(4G{ z{KBf~)|3u?OWM=ezF$xI;mMRCXPSlMVX-{I%x?Hxd?-A-7_Hw){r$ zGS%$te6;P*(^;bAW*l2DIT+UK-M>5VB(I%S#cCScz_nBh(fG7F0+GugDm6XcIj>Uj z?cL$`n51FJK$VT{G{TF(02<=D+OLe_vQIG$*v@>`J9@|1vDJr3n*J`Em_%NEq#*f7 zevHxbJrJ~lYgk1qt2WHn$B7Y>@|)I!d;!#64mMNm`kJOy1M^udoxPRNI^>7&VGl^+ z{MyEFUq`HI$O=0qg<;M-a+k|MXIH3_m{s1mZ;6ub70P0on?;4G7QJTYjUcNhdK1jrNRQ&*1q=MwO&oF_>R2Q`3jXP z-fBAco1t-WfeTq8pf;_}8O!J7%g#A>z*4#4`00!A0?~}VwxkZpdZpNWcvBH1^0nhG zUVxNukFhui*z?xr1lFW=SX1QSNBgR3X++0p;D`(azgU zrH+ug9ON?V#F-{erwc<|>S;dn7mONAe_TT0lSi9p7fWP(#`D1aT80zBq4t_)Y??|z zqf52ryUk)#d5|(Ajtj|)^7tKfaj7b&zoCZx#IOWw>*^vkKdyVzl#OkgdeW}Gw4Bdr z=nX4kYiv9c5wQE~E%Th~U8;-wrQOIC1&Qd0qJF+BXEs#N4P=JA{(b)D+3S6b6XKE` zfjbWvRWM6QC=M_Zm_$=0Oe9uWEd&Mo3TPNN6L(u2QShwKx45esm3|2(ZwSknX@Bc& z3Kar-pI*U#pImxTijvl^kH*xO!fWtvdLI^(7)|Z1>f&aFC(5W6^c=M0@0=WL^bm8f zofT1op9bf-6I~zf_exl1T6R?5-oEIp0Zv6!(i%MDl6~1du<;W-z8n10~=0bPogNULy^Z|A)&cQB5|F8PE_l;n%q zYti?0-u3z$z;}+$5ovv)|I=+iAO!~9r)>MF$Jbc@WF%0Ji;Ix%9x@;VLSD88&|UYEirRz)a2LqM9mH{5XYb_?&M_m%f0W!Na=Q->uxHm^3Gv(8O?CP`=f^>KSci7sGO@>})` zn+r1t&cwH`r629H)X|M~t>WnV*ofnIgG3-u7%WLhqDCb61M(sXy9M~*Ft?<-T?yJ@ zG4pmPugWEJnbCKW0Jo1(pW}D+pCKn`LNI}Uik6y9nw^5RM3*(SC1@1R6BBqw zFS1ssllXS^#8mqKmdr0;)afsk|s+CKi`9wt-+TC`Z0+RgJb_>ABfOTLD4|B U*bWLRMcV~AZihkB|5ojP03FkGfdBvi literal 0 HcmV?d00001 diff --git a/Meshtastic/Resources/Certificates/ca.pem b/Meshtastic/Resources/Certificates/ca.pem new file mode 100644 index 00000000..1dc6e36f --- /dev/null +++ b/Meshtastic/Resources/Certificates/ca.pem @@ -0,0 +1,23 @@ +-----BEGIN CERTIFICATE----- +MIID4zCCAsugAwIBAgIUeM9XhqZCtta+QorYNjZSdAk3gkMwDQYJKoZIhvcNAQEL +BQAwgYAxCzAJBgNVBAYTAlVTMRMwEQYDVQQIDApDYWxpZm9ybmlhMRYwFAYDVQQH +DA1TYW4gRnJhbmNpc2NvMRMwEQYDVQQKDApNZXNodGFzdGljMRMwEQYDVQQLDApU +QUsgU2VydmVyMRowGAYDVQQDDBFNZXNodGFzdGljIFRBSyBDQTAeFw0yNTEyMzEx +OTQwMDJaFw0yODA0MDQxOTQwMDJaMIGAMQswCQYDVQQGEwJVUzETMBEGA1UECAwK +Q2FsaWZvcm5pYTEWMBQGA1UEBwwNU2FuIEZyYW5jaXNjbzETMBEGA1UECgwKTWVz +aHRhc3RpYzETMBEGA1UECwwKVEFLIFNlcnZlcjEaMBgGA1UEAwwRTWVzaHRhc3Rp +YyBUQUsgQ0EwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQC2F6/n1CI2 +4dGtLt0irkfiU+PRmqkkuE7m49i7/FeH+38SEn9+0B4egW0kYRoRXmYdPzRsVttu +23LZ3RLjwB6fFI3tiA27mxD58AuEMfwVR7J29oHqFwuVhuqDyjkNpUPFUomKwzvK +SPJvoiHGkbQwWTMNP6T06tCg9llSE7SIgJWjzikQ+JsI37SqVGZ8K2evs7LTuyQh +ssJfYVB7aE1kNNyi8YFHLoCWQMB7h8qJ3hRd7QGFG9gfWuNrWtim61iiHgBAPTRw +gMn+YSIZiV9/iOytBKxFppNTxffEowF/iKBvgXwd9KHxYkk1Nvtcz5NJynSL75PT +8B7XiHCGhcgzAgMBAAGjUzBRMB0GA1UdDgQWBBRRe/o9Raj93Fq22ArNSNrpsye3 +AzAfBgNVHSMEGDAWgBRRe/o9Raj93Fq22ArNSNrpsye3AzAPBgNVHRMBAf8EBTAD +AQH/MA0GCSqGSIb3DQEBCwUAA4IBAQAsuSQ+j/1Bm7HbZWzN5qChH554vucWoqI0 +sVRHThvCASC6+wSosWZlx/Ag5KnRmBVsYA6CX5ztoF5keiSRy5G7qyRQVjITOq1o +4XUAHBtGxKdRCEzS84GnsW9qeWX7t/xxf2fFr9gPZ7Z4nuyNg7QyX5FM01BtAlZC +HbBhXvJyHRqJkMe7keYU7GmiAs1RZa+7593uEQ8DQ/kRvCzU0XswFSguJrd4Fnpi +PGesGOk0NHFQY9pIu9oshgPgMA9dEWnhhvAF3PZ3sLRn9sSuslj5oumFsTYboByE +aOKQshFe5xEX/4O7DI+wsD1Pt5gdT75nAuG7GEAIFKKGjQtUUYfH +-----END CERTIFICATE----- diff --git a/Meshtastic/Resources/Certificates/client.p12 b/Meshtastic/Resources/Certificates/client.p12 new file mode 100644 index 0000000000000000000000000000000000000000..2f27bff2d6a5e102bb7f6c343c6a1bacd530a01f GIT binary patch literal 3827 zcmai%Ra6uV*M(siy1OKXZf59~lvYv(kZ$RghCv!6hejG1X(T13q)S2=QV9`|9vVKs z_5bUAFTabk&f0rloV&fA1BMYD0njnPFrsoST%KsP=u1L$Z1f@+Q6>;Zl=jzt2ZrI? z{2O5v!EpBfTDt&rw7+ZP-vk|O@sGeH1nYqz|4ITd8<6B>YXf4p8|-+3jMm>|GPjeHRQ?95lw%h$R&(@2y#h=y^xPoE3(OBv+Y2 zD0|sQwbe&|WtF?V*63pki3&NW6zs6_NyalKY_BSDTLK}&&Cd=uJ?qJED9On0zr-;Z zSL=#op>@>1l3EJ&)F{HD%Lv>0Gr7#R{ARcX^dt3xmh%{^xOdXJ03p#)ik;&mbmM0w zw_RX=O@dRWT_w)X6vyUColxL6;`puz)f_Q5QB?yUj2+a>Rmt?QPZ@ll-%zgpv-J?o z0sE<=u|O*p(6Z@$&)ze1xrdATQ^SLlvoi_G=Gu&}_V=GDPbIMjys!Ij{Do z(@yPqYxj7N^m7+@@*g#|O;YA{L|kEyUWGvcMp5foof+3`O^U8iDF4CBI#9*FjSpTK z(kV;RlA;rXRbWFF7|Eljyx(zSoj7{sAI7MnGDMujI9(0p>zjZxu>EdPcO}n?VaC5x z-9iNPB9BygVFRC;Zjs!L5KsBSmCbd>v=)U4nR%Sa$sIb)qS{R4Hz=zOYolC}#)wij zd;{U&2&sXWs_pL5V~EOH|9Yl5takQmjiqebepv;l__d^Hq9y%h)=mOV zzm$_k9ZSD#Lh^N$AVxV(IsiH*sTaNXkE7X+-~krY8iq52t{_#I`JS3fYi∈>}Zi zLl@B(^{PI`XFin|sqOv0KkCnahq`pn*v9No{XhlMSeUrrr?5BoniGrI&7hT+Byg#l z4sl*{e2$%9?J;$eo=HtCc+O{N`g`2?RS)gMCwty`t|0C)^**gjv)e!R1U`4`Q+bIW z(|tJGqgWJ~HK_O#1fc_W7Q$^eW>-!50o7HZ>lf>>60e3ejaoLbe9BlxRMSh_Kq})m z*l3^zzpBrRMY#RkkRD}@Z8a`lp@;2e8QjtEW2pybo;33 zzg-yn0ubF8S}KeE2Y;8z(o2BLl0}V$x)T;5!7pVeb<`h#KE)ZDWHtbwG+cb(@1xZ6 zUR8GZuFe4~`MUlzG0kZR8PEkqN@E2zVMs|wjjEPvD@ZNxj>}Y;;iffrP~#4sOrHtE z3HxdbOvNlsUD4Epo$V7TYOM z^l0(f+I&$(Dk`5GFt`n$l|C+9StshwnikA6cg1^JO0yak8L~nD@w-&@J_Z{@S-I#; zSu6T7g{CwVKC>LNiviHw^C5<%QJEMGoew~I>YAeYX6Zo$Qvr(HwI&R z$FQ}7o=0d^5Mm;iawERung4Gy*4$3b(p*YALWIoZQ7S?;kpE|pxf$#CeR-h|9+}0M z@=rGoiz}%u!kVz4)siJTy4|U&N&qOTUx@@@=PyE%ag8g)gTQvWJ;BC4&&dguH3z7H5F1Z*n_t!5<6ke)W*xO_VY4{S7EG#8w zq^gyl7T^DNlsaJ>O9-cf6xc;1r41wlx^k3hR>HO`^sxzT+!#`H1a_Q<%u`6CH*7pS zXwnl}9r+iwI)z+$ssk(ib-k`TSbyYjrW{O^DFIRGJ5U5p!6x5lmiGYJJc$AfuskPh}AO1aWmN0>dy9|KXVbf)Qp25QZ7>*Y^E8o>=(*&rAe( z=zzbWs7j$G2zwp$(7x{d6;ttlf>2Jy7RrjzA&)`BT&zB8-vbB(P7^QH zzbme~8!0Z9v9FL?a%%sz?m&t*UZnbgGgQOqY&%zF^JsXfK7xb+#zBan!k>2dR688RnpJve9i=BYac`LveawsI8j2}jEKs?L>Aepo`B)nd|%)Hl}pa#oP8jT9*y?S9|L>J19L9BO@4xQ06poFjCVv z$p9-pNPW;VGMPvblbzw4Z~=XW(y3+Mb_}hVLG6Pvd+05~!BTGj_*^V(XX{2XyU&716)b;+*``-Vt2S6PrWyi~1D zf0GG;{VavwUizK%_zF4sQQ_*Jj*zGG_(ZyuIfhD#vM;BOkMM4l){V1J8FfgD(H*@>a{c^!8m{WO6kGk}8iMqOA>>>4#= z-myZlHMA#m?u4b^js9p>CD^ zvK}Dx0>fj1eELO^Py=ULN; z@s)X(Nw!N=PEjdy_WiSa`6e%onppf!za5M1=gP}6#q;-a;Weu0(rg+1Q?`AAJ$J)i zgm`+Yo8l2f2f-K^ouH3 z0^x$$2VhgUpSk>{lXb&!MDVe$@bSc%Rv<)omE16@pivG|0!BrN(dE@Vku4{Yh*f> zj4(qQdHGonI!0Z+~(BP>IVP2_LZC&a4 zfQZJ@q=QPPi#0I@M~>YQGW(`s(t>D70AYAN<46FF-SZskQq3D)u$N!Bu7H2pZCSE` zfdo4VhHPcqyEQN0W387oXmDp3h1Bd5tYkP#qjJlyjjk$f{iGag0+u&65;Oty<~pg| zZx;<@nC)AcnKFLO;ly@jEpDBrRhvBcHr_tbIvwka+`hgad?RONPiR-uJ=}b@v6=B? zj)JO~r-a}V-4))-K(gO2AUJi3QcOcV#JKB-(p^93C?vh}0v#)kKVAPZ8C>D~#fNNj zd*hc(Aoo@dld;^NYbV55b-y<>>RH=mx58PqEpOiS+)Oi2%Guk4yt2p*ZC3YU8DIS5 zfLC@;?R^@6xoM@M^GuhD#ZuYzRf>km@W)LA>>|(Mfw-6ut!wX;hSng50o{oL<8!F< zJNKQ7+X_d zIpX_^V}fqmx#wCDE%$n*6E=md!!PAgfp~p)9S9};CE>m!ZdziFblk_*m(J+%K)$2y zTF=9)6YtHokOVpTCdXFRQ={n3C*%ps#>-}QCY1XoNuq~E32Y&o{#g=zCB9A~eLgtl zC2}Mjk+~P;Pa1t2t5+I1MP?B21j9cY+wS+s+J>g*5%;L5_z0^~*0#$7rnp0037K0n zpVvY+2VGb(coiv%1IJ(fTc_3J1}z?Z5Z_uLOP6eukhEJjlKEHg{Jba1TrB~nN3FY5 zMk)-bj&cchJFl{M6P}gbGx;R*afz&6Z08v^mSg&vz|+nry=zvY32L%t1sJ_Ut3X3;m#9MK5D#4;&=aolH&{UcmS-Oy>p2gSbma=RIgYRN2<`0a`nD3oNiNk39 zA|Ho?Xnr(skKGt}CmEFeMvC{zF#S)HFd8>%g-8U^w@scV}F>ap|-e?YZf>dMqUJ8F{BwOJj>&w2g1 zY7tm^Uc8{M)}*=uiCyUguNc;R2)oGsj=60>I+3soO{n=pZx*tb!VW26zlzWf($^PG z!tKTrl(RH(*SuSlDhukJP@H@29fy5#Vlcm3$?*2jRmMoSerl!RsxglUK;W>We9L`4 z*?#|dBA;SmZNE>!(Pq$}0z{zt+RaFz9pWD#!+}>sWMrH0w!+8qv_$l-Z(tQ=&>SBd zsx8`0&Zc)x8@_z@T68H^<2_Gn>pA%G7T2aIBHGX^{#e)@;S=CluoAcOp@zH?ZWX@FLT*%G~? zMF%JJsqXC!{kz`jmi?UYQx3?Rr?199=-^dux2Vwb2si)!*Fdo?`kw>EG{q&tb1QC6 zLy3DvFPliF)V|GWRpFj2=)xC~N>N8>LKV>IM6mBr9KrLwyZC4s{hKYRrI}a+Hi{dc z?sqIn@bp!a&*k&{;jLr)DZHTB?2OwvXTO}Lz9e+7+&&07lhPq7Zc zx|9dc&>FI@MRbyIC4QGFeWybNO_< zb*X@B5sm^zxm2WwJ6RflYIHb8ZOWkwv&V&L3Ri22l?*GRbIFQchWSUIWyL$CL`?Uw zjPjORs>4DK1(%n_4EtgV5-qye^#Z}UY@UWJpIoc(RY5YR0~^akm1p2fDw9`f+A2o3 z%*F7Z7e?3!v(zuQ;@3%ybe6)uup^4+G=q&Fj8$oV&)v>zG>RgJ(ZARz&TdT#lV*gJ zEE);_R`!1BgtN%sn-x#@MA;~OS8Ka?Y1049(=naWq^1dk5qid0!kUo?ALc)@w=$jf3fUm`xV&t8) zeoh@QwVszRcT6{}tZkGLoPnZG3LT)9LKyLY$!(DLiBgDXp(l_mX@ zIp<7#Kl=q>!3c89nk16KWn?YC5aHB@#VDURd~HB7x$Ka&>`Q`;@75X_N254s^c3`t;N7-TX&` zW6i&pYe?6lAPKNZ7#8^CAI|wNH~}L7SRm%F9sGChgXsUCsUUQeRDVO2zvi|71xnVe zWEIou`Z6P1DMa)E=*Is9rR1_T;qnzOflOpDj zE#R$tb23Dg99g#LB(0JUmsWSg!s0jIr}uxSOP1%juGv53fEMmH)ABEET;adtPwnEC z3e_OZqx&Q&o{xr;o_7%0t_ep(wo86lXnk`h>e*dqO!5)C<+|j~yQT^2?D;i1&ZEwV z*c|RoixS}FfY+K;tZb`}5BS99n5$3FWii2n@dPPxgKb_9rT8aIIqN2G^N-z@Q=&=t zDDNAHf<*h#YDp5n z`HwrwH)2NHxeqoi*)MV-CF#bi=--2|<;)jihlBY_m%HG(=^5qTcwNyCjRp}NNcm`f zh4gBS(peNHfAlc6u^;8+ah97W=qyKKTQ&k7O{h6*+>G(v3b{UP?+@7$=SXf z@YTeOR(Lx3Ox=GQRPBc8Qp{o*zaKk+nseUp(!6Ij^-A(C6X+2>(?XEEmD(}HPiIK& zJ%r*&vQwbJRW>e|kK>C|Yje)mdg9`?ytrK0$t=E^GjHbqL}<`gMEY6X#MZ5sLHOeb z%eO1DKwO6bpb1^ToYS2>+6<#BmS0=cODYxL7o+yqm@U4l2saoVE$l=*RIMB*E*kmBHNgIXHUFY;IY`SBq#&Qjb&^;MFHb)sQ{G8(Pm6(-jr3e7OQGtN$LUKOG{ zCRQksZP7|Fb}X|URXN`=U&-C|Du1V_aYdM{S+CS3*j_8^l>D7~8uD#2Kasq|SL-fN zzY6<#ov`NvHnmMws_Yd?daSHEtULHJsfVameH*VGUQY}9+8J#MGj zux;k2ZW~)3Zt%xAI0mc%d|%QQY=1iEZ(>+kGZPT%gJi=?wEGC8d`}sJ#`3V%iA*wi z2&@-VKmA-S)va~0@1nyU4SCi6}0F^sRH@zEZW2ouc3=TqDV5LwxgVq!MXJ zg%`LY>dEulNQ|&)p@oFAw)<*3EU4xmk%J{9dx6qP7~2)c78w_$<6frlEnL5m zLg`bU@7d(X%bJLIO_?U+e&VMtzYFm6Gg2hli29Y_-1BCbLmnTFxN=A|`?;Aq zvnJjpWnvUn-#XG=3RT(nL;mGDk$;MomR*hma7k=uGr&dljJ_wwCso}64n(AUU4!Ys z( @@ -458,6 +470,7 @@ struct Settings: View { developersSection #endif firmwareSection + takSection } } .navigationDestination(for: SettingsNavigationState.self) { destination in @@ -521,6 +534,8 @@ struct Settings: View { AppData() case .firmwareUpdates: Firmware(node: node) + case .tak: + TAKServerConfig() } } .onChange(of: UserDefaults.preferredPeripheralNum ) { _, newConnectedNode in diff --git a/Meshtastic/Views/Settings/TAKServerConfig.swift b/Meshtastic/Views/Settings/TAKServerConfig.swift new file mode 100644 index 00000000..37ccc861 --- /dev/null +++ b/Meshtastic/Views/Settings/TAKServerConfig.swift @@ -0,0 +1,390 @@ +// +// TAKServerConfig.swift +// Meshtastic +// +// Created by niccellular 12/26/25 +// + +import SwiftUI +import UniformTypeIdentifiers +import OSLog + +enum CertificateImportType { + case p12 + case pem +} + +struct TAKServerConfig: View { + @StateObject private var takServer = TAKServerManager.shared + @State private var showingFileImporter = false + @State private var importType: CertificateImportType = .p12 + @State private var p12Password = "" + @State private var showingPasswordPrompt = false + @State private var pendingP12Data: Data? + @State private var importError: String? + @State private var showingImportError = false + @State private var showingFileExporter = false + @State private var dataPackageURL: URL? + + private let certManager = TAKCertificateManager.shared + + var body: some View { + Form { + serverStatusSection + serverConfigSection + certificatesSection + dataPackageSection + } + .navigationTitle("TAK Server") + .fileImporter( + isPresented: $showingFileImporter, + allowedContentTypes: [.item], + allowsMultipleSelection: false + ) { result in + switch importType { + case .p12: + handleP12Import(result) + case .pem: + handlePEMImport(result) + } + } + .alert("Enter P12 Password", isPresented: $showingPasswordPrompt) { + SecureField("Password", text: $p12Password) + Button("Import") { + importP12WithPassword() + } + Button("Cancel", role: .cancel) { + p12Password = "" + pendingP12Data = nil + } + } message: { + Text("Enter the password for the PKCS#12 file") + } + .alert("Import Error", isPresented: $showingImportError) { + Button("OK", role: .cancel) {} + } message: { + Text(importError ?? "Unknown error") + } + .fileExporter( + isPresented: $showingFileExporter, + document: dataPackageURL.map { ZipDocument(url: $0) }, + contentType: .zip, + defaultFilename: "Meshtastic_TAK_Server.zip" + ) { result in + switch result { + case .success(let url): + Logger.tak.info("Data package saved to: \(url.path)") + case .failure(let error): + importError = "Failed to save: \(error.localizedDescription)" + showingImportError = true + } + // Clean up the source file + if let sourceURL = dataPackageURL { + try? FileManager.default.removeItem(at: sourceURL) + } + dataPackageURL = nil + } + } + + // MARK: - Server Status Section + + private var serverStatusSection: some View { + Section { + HStack { + Label { + Text("Status") + } icon: { + Circle() + .fill(takServer.isRunning ? .green : .gray) + .frame(width: 10, height: 10) + } + Spacer() + Text(takServer.statusDescription) + .foregroundColor(.secondary) + } + + if let error = takServer.lastError { + HStack { + Image(systemName: "exclamationmark.triangle.fill") + .foregroundColor(.orange) + Text(error) + .font(.caption) + .foregroundColor(.orange) + } + } + } header: { + Text("Server Status") + } + } + + // MARK: - Server Configuration Section + + private var serverConfigSection: some View { + Section { + Toggle(isOn: $takServer.enabled) { + Label("Enable TAK Server", systemImage: "antenna.radiowaves.left.and.right") + } + .toggleStyle(SwitchToggleStyle(tint: .accentColor)) + + HStack { + Label("Port", systemImage: "number") + Spacer() + Text("8089") + .foregroundColor(.secondary) + } + + HStack { + Label("Security", systemImage: "lock.fill") + Spacer() + Text("mTLS") + .foregroundColor(.secondary) + } + + if takServer.isRunning { + Button { + Task { + try? await takServer.restart() + } + } label: { + Label("Restart Server", systemImage: "arrow.clockwise") + } + } + } header: { + Text("Configuration") + } footer: { + Text("Secure mTLS connection on port 8089. Both server and client certificates are required.") + } + } + + // MARK: - Certificates Section + + private var certificatesSection: some View { + Section { + // Server Certificate + VStack(alignment: .leading, spacing: 8) { + HStack { + Label("Server Certificate", systemImage: "key.fill") + Spacer() + if certManager.hasServerCertificate() { + Image(systemName: "checkmark.circle.fill") + .foregroundColor(.green) + } else { + Image(systemName: "xmark.circle.fill") + .foregroundColor(.red) + } + } + + if let certInfo = certManager.getServerCertificateInfo() { + Text(certInfo) + .font(.caption) + .foregroundColor(.secondary) + } + + HStack { + Button { + importType = .p12 + showingFileImporter = true + } label: { + Text("Import Custom .p12") + } + .buttonStyle(.bordered) + + if certManager.hasCustomServerCertificate() { + Button { + certManager.resetToDefaultServerCertificate() + } label: { + Text("Reset to Default") + } + .buttonStyle(.bordered) + } + } + } + .padding(.vertical, 4) + + // Client CA Certificate + VStack(alignment: .leading, spacing: 8) { + HStack { + Label("Client CA Certificate", systemImage: "person.badge.shield.checkmark") + Spacer() + if certManager.hasClientCACertificate() { + Image(systemName: "checkmark.circle.fill") + .foregroundColor(.green) + } else { + Image(systemName: "xmark.circle.fill") + .foregroundColor(.red) + } + } + + let caInfo = certManager.getClientCACertificateInfo() + if !caInfo.isEmpty { + ForEach(caInfo, id: \.self) { info in + Text(info) + .font(.caption) + .foregroundColor(.secondary) + } + } + + HStack { + Button { + importType = .pem + showingFileImporter = true + } label: { + Text(certManager.hasClientCACertificate() ? "Add CA" : "Import .pem") + } + .buttonStyle(.bordered) + + if certManager.hasClientCACertificate() { + Button(role: .destructive) { + certManager.deleteClientCACertificates() + } label: { + Text("Delete All") + } + .buttonStyle(.bordered) + } + } + } + .padding(.vertical, 4) + + // Reset to bundled defaults + Button { + certManager.reloadBundledCertificates() + if takServer.isRunning { + Task { + try? await takServer.restart() + } + } + } label: { + Label("Reload Bundled Certificates", systemImage: "arrow.triangle.2.circlepath") + } + } header: { + Text("TLS Certificates") + } footer: { + Text("A default self-signed certificate is included for localhost connections. Import a custom .p12 if needed. Client CA (.pem) validates connecting TAK clients.") + } + } + + // MARK: - Data Package Section + + private var dataPackageSection: some View { + Section { + Button { + generateAndShareDataPackage() + } label: { + Label("Download TAK Server Data Package", systemImage: "arrow.down.doc.fill") + } + } header: { + Text("Client Configuration") + } footer: { + Text("Generate a data package (.zip) to configure ITAK or other TAK clients to connect to this server.") + } + } + + + // MARK: - Import Handlers + + private func handleP12Import(_ result: Result<[URL], Error>) { + switch result { + case .success(let urls): + guard let url = urls.first else { return } + + guard url.startAccessingSecurityScopedResource() else { + importError = "Cannot access file" + showingImportError = true + return + } + defer { url.stopAccessingSecurityScopedResource() } + + do { + pendingP12Data = try Data(contentsOf: url) + p12Password = "" + showingPasswordPrompt = true + } catch { + importError = "Failed to read file: \(error.localizedDescription)" + showingImportError = true + } + + case .failure(let error): + importError = error.localizedDescription + showingImportError = true + } + } + + private func importP12WithPassword() { + guard let data = pendingP12Data else { return } + + do { + _ = try certManager.importServerIdentity(from: data, password: p12Password) + Logger.tak.info("Server certificate imported successfully") + } catch { + importError = error.localizedDescription + showingImportError = true + } + + p12Password = "" + pendingP12Data = nil + } + + private func handlePEMImport(_ result: Result<[URL], Error>) { + switch result { + case .success(let urls): + guard let url = urls.first else { return } + + guard url.startAccessingSecurityScopedResource() else { + importError = "Cannot access file" + showingImportError = true + return + } + defer { url.stopAccessingSecurityScopedResource() } + + do { + let data = try Data(contentsOf: url) + _ = try certManager.importClientCACertificate(from: data) + Logger.tak.info("Client CA certificate imported successfully") + } catch { + importError = error.localizedDescription + showingImportError = true + } + + case .failure(let error): + importError = error.localizedDescription + showingImportError = true + } + } + + // MARK: - Data Package Generation + + private func generateAndShareDataPackage() { + guard let url = TAKDataPackageGenerator.shared.generateDataPackage( + port: TAKServerManager.defaultTLSPort, + useTLS: true, + description: "Meshtastic TAK Server" + ) else { + importError = "Failed to generate data package" + showingImportError = true + return + } + + dataPackageURL = url + showingFileExporter = true + } +} + +// MARK: - Zip Document for File Exporter + +struct ZipDocument: FileDocument { + static var readableContentTypes: [UTType] { [.zip] } + + let data: Data + + init(url: URL) { + self.data = (try? Data(contentsOf: url)) ?? Data() + } + + init(configuration: ReadConfiguration) throws { + self.data = configuration.file.regularFileContents ?? Data() + } + + func fileWrapper(configuration: WriteConfiguration) throws -> FileWrapper { + FileWrapper(regularFileWithContents: data) + } +} diff --git a/itak-example-data-package/iphone.p12 b/itak-example-data-package/iphone.p12 new file mode 100644 index 0000000000000000000000000000000000000000..7f836b2f732d3e19103bc6dd4358d55d5b671d7b GIT binary patch literal 3009 zcmZXVcQ6|cAI6P{5Jc2gidv;eL#WvpQF~LP)UFt{RnZ@U7*Qkks4AssiPh4mz18|D zF>2N-YE#sD``*3l?)~F?&)w&_d;a`>U>I-{1vw=Q1C9aGh+;Ld$ModXa0Q;?JW3HtvwLbzvF9X&XHHP8QROWr$AeUeUaIf}S-agDMV$1e3RjW{;#k7e zn@u)6tY5m?KmE5i&tR~^{u*z9w1pYkaH}LgN=T(t@ZF774xWaZXX2Slq!9?C9P0RQo^4kOI8fgRZxU?alrf73nt?tEYY@K8XG0r z2HNFW6$E~^JTHv3#d5G~P4$X?EK_^lBlKdw{A(Ix+~aNf@tiD^zNHA(Ews+aGbp14 zdM|y!W5Hsfbp^s{B{6AJ`a-Jd_RtNIKKo1?A4Hg9kuy+I@u^g{!)glC_-EP5FnB7m zVx8uJ@u{0_vj2t-6@4?VPyA&R&J_{`y*d{`TkkcXjHZQ@u6I#a8 z+WsCpk7~Em&w|U=W@W#v`4oZfp7vo!N0|ATrP%X$;G4N=)Pp%Zn4q`Co_;`?!8`?P zB`l-5)Nvt~Cf8$D%B=2@mN%0MS*k6Kxe9&x)PEwEqTip-bR(@AZ_L}ofWFczUR^{6}cSC|>* zVA5-!BB4mIej!bf_`$u{O#WNI;*Si$C*J*=Jt$$EeL$$BTmCSC@8goC$9zb|Jge;)rIAfCOu3C!~M&9b9@d46`P1lvK#9^e8l~r8DsEPT%rhR=$FU+~eG) zh(_?u z^B;0Da$#G%&CLx}8BQ7!?~XFXKb*c1y!YxZ=Yz}`8kb-inFC&>vam1#Qc4i~yija5 z8|d|n*bDQXjaTsy+`aYi292F|6@PyA)dtXBinMs`0`E`U_@pM&K-J(#P8`?_Ry|sV zW==AK4o(}F3kz|$TR+e)q?&fc9XDPAr9?BT1j?`QL|-G4tL#33`@wMEd(RSh$@7dL zjgo!2%ahpr=M$bc{;~pYiChV^e_h=AYTEzSAWg^_!y{#c-gt_u@ zUrXJglVgg2P8Dbx;D@#>~MTB;98W5=EdHx zz$$({rWNx>Ee7hxeV6^HW`Y6drp!o%HxDCle=Q2AHrzQAWn=ex^J6lUM2@Te zsT9|zHODlmb=Ns#s?+73jC}XF6!-BX3g{00wzKr#f?keJA+79En%Md=fh3npt-?N4djbJ6J;5e zdx@L<=_7v53mepYZKu(op4#}rTGfCz=SxSj=UZG&bDC1D{l;I?6E5#Wk3~A!Suj0v z`zczix49sXag%%BIOBj#r%y;hSxXaGk3-$7rFI{^CkxgV;x%1wDzb4s?T2BgRQ^W@ z1sEzg07E752aEnWAt3nQdY~nzD8K-=VHm*1|69YX)EwD^5EuKWH2@eu@7m-e>dMaA zv$}FOa{giRb-qgzoZ>c5!F)|z`&1~3g}MsY7auQBo-nmunirQ$q(Wy#GcHysy6Hd6 zgcbe5n`U@{q)MfZieoF(;M~2~`^Af+fgiF0oo*a|UUBl$u^AMJQ?V}-PSR6PMFKBeYnn3FGm2^uOLy$#e&y_%3LTJHBWNwAqn+BuF zs0^V|iI)Br&J2alXp48y@=oWSmi^X?T%w7EJ70Wvl{Ae25o=mkUTCc-;ZI|Cab)&Y zDydi9Kk()!O<`)xqH8^o26d6w2fIRkHLE8l^fRZRjYVO>G@UI8v#(~WWRaZ&(l0AuKvxh` zfC#Pe#8FyF=E~o~W3G%7_-%EYrc=2qo{6?b6LJ%VT$a>RsA>Z8KL*#hK<@$VG+5Tpz@LLbRW;;|*3)~6>Lvka-t7<+^kCH(xoFmA= zwz{tnuP!yYyvM%JalUp}Pjt1sm<*_FHZrtl8R@P|u8Edji>=tmqc8itko0ADrBF#% zzZqI@UBGqL^=@v=-#f@d7a{**vUI@ce{`Ul`MhkZ-;zR7{`3m zj6^2GNz44AmoL%nQ8;xvb+$@z+-&e)=9E_Lfj8rALa!7I-Ym7^Tfd8j+j;H!Hy6&X zEBEv`<$N?;sI@L2v4Zb&4$IjwX9&)BP^gkEQdJ~JKai8MnzYW)e7!cub5l)`&}2oWWVj0rvdpf%}OO zy9OPe*fhj^y%TjOSXX>vyLrsx6m3h%TgoKnes@3<8)|REF|%M`lJtJlfwrjKd35nz zGtt@1V?NRgQbdWk3Iu|E6x!TvU`g<7E9($t)Umhu!*1iqEv>e{=-d;!aXzIiadPuG}yAsbeq*nF+Qi*+u zbgiDNS}Us9aI;3PHwy4fzs|ZN2)p^8??}UPhZVr=lRxoNt@N|62caY@RtEc(-a|1L zCJy6+(EzE$7$_+?smK7#4*KAB(W0bPn%(0kUPlY)FJ#HF0Fb7*9E09UkncYL(|DeM literal 0 HcmV?d00001 diff --git a/itak-example-data-package/manifest.xml b/itak-example-data-package/manifest.xml new file mode 100644 index 00000000..f356ba95 --- /dev/null +++ b/itak-example-data-package/manifest.xml @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/itak-example-data-package/server.p12 b/itak-example-data-package/server.p12 new file mode 100644 index 0000000000000000000000000000000000000000..913f4692aa346b4d3ac7e964c0764f8e0bd430b8 GIT binary patch literal 3009 zcmZXVc`y_XAI9xk>y~3lY!>Uty%rI2C-?5slAJAaA7zu9mRu#It=}(lU+Y+k+|oKW zmWVdWl^nUrowK)j=e=g$Kfd$Ke4d%-&+i98U~dBg7!d^aL@=vtf=R+YCx97{M_>;H z5!ms6a1eq3mi})A&Le;Y{~!nu!0;zH|Jwi%P%zuS7C8Paa)DSVOc86HK+!lLkcAX>)GK|oiirh{fY$uTOTzL;tE(ykktHjwA zMU=9&-{{$a7X0FNhu9_E2r%TGjdyuelh^LdlF6hQ*?zq(Hu$Bq=7ZDGG##h7C{z2x zK00wB&;K{VNM!d`H9=syO$!C-jEhz)jwa^c&E}Y=wIYgM zyOo(hY44Zt6w!M4Nt=|b;@D9Duk6rUp zg~y4d6?ns*9?faoZGe<&V$)?s9+)`nXW(M0#cE^Yq3|1rbt4Zgc8EcPxxqI##SmzP zU1sH+&P;W5GhWX%eco)@V7-aK-Jzhc00;NjuMX0h|f!%TKeegY3%MxoU(;8+Z*NA-g1B~;j2~c5MnfW>2 z_2_ZxH8u2-aes-9dK0qOo@4qkN2ICH_aK92_pr^6&x7mmfzbDEl*h8AdUZEt{d|l& z6WUUH&Dp;q4Ca}?ED_Tpo&h_H3$Vp#GQQxRuj0gT_s~4q6!tL4GOFFoLW-QO0GpuQ zn#cr=glJN}uS}fPHGVa3w6Os0Q&S4yTPG#%{}5G;>Pu>Z_n-3@85Zgq=C{>alfBmE zE39D|JWfQ?!RsB|+J*UeYY2BIW18L-)hfa0Tk3Zb>(q-{se*w~5U!k(ZX-#_cd@a` zD24+w$LFGkQf!O>p!JvD09nj_aY=m!ndjw(wi_u~N345iA)+T+WZTmA9WqHu0<)6S z{$Q{!rXr zAOOzK!M@pn#%--7Pe`-Lq7to>;pW4K*WN3-zo)dGK80^`TUa&0qG)GFp=1PsN&kO@ zkVjxbf(T5Cf3WPIlLE8*Uqjv#=R|KA$22P;0nu`mC$28aOa@lty%mLHtM zaS~^p$(f(65N4>67o#L#c!lo9uaKOj5^VB_9Z!KKpsq4vPhYHw5;ETCCmdgJ(byr` zV_Bit_^(>GMabS|*#-q$!($2UR!4>!E2jtIMs9Bc*(INf6WD5r{Cl5^rk-{!@A66Z zquUnXKjbk;7HQ8E^oc_cFGjWTfS3{SF?KSRuPJqqHCV@PpqDv`3P?#raNJz!)Dp^7 zfcYF@Dl0jHPwP}OFEWqm2G&Q^PMG=!g?8S4)fYr%5qpBfEn*zf1R1@}9M z(aWp8@DN@;Fk`Ht?h23~h%eIbdO4m#ZgU)e2otGqipoIcGq2@)5=%GPWH`a(Js@Mg z!L^oy(2kn5OC3(J69StSLDmj%W;{0Ffwwv~ofhDpp zDo&9E+>+;zP*MfNQtbBEF;@>f;7O4Gh&Roql>Dd9~D%XI&xG;%Rw%zOd`0%ZuQ{r3wmMaeVfP z++xSKjSc?Rm*cowBG{wdPp;!FnVQ`XDkPynTw~zYLT;C5xmkpGDdD|v0*RwZ`7ZEy zA$rT{QO4VQT{Aw?DQ2!y0=R-iS&vl2Oo>2Duqm|PU;lLGvSw83IL|YP^z~PE$P2TC zI%`%1rrj^+w$&E38Iy*aCs&~m4I!>{jKqk+pj$z_ch~OAZU9Ek!mdOA{HHK8Ou-~q z^Ugdf*|g-_e&7?MUu}G^VqaIs>_=^k+A6!YiSLvYQ(L&twxRi5ibw0&1kMaWR=SE+ z4h8X`x3$sp{f^t}l;u#xb8KYk{07t6g6*;`krvvMK11qkN+ZFjL)4mfM5@#BP+kM00<(?ckT3kfZh92;KQVBh*lVzqxS+r y0U?H91vANUF#?5{7(kG_@~bd`V}N#- + + + 1 + Win10 Taky Server + true + 172.30.254.210:8089:ssl + + + true + cert/server.p12 + atakatak + atakatak + cert/iphone.p12 + + From ce942f510be7d75948a9abce3e6e15750c23d889 Mon Sep 17 00:00:00 2001 From: Ben Meadors Date: Mon, 5 Jan 2026 12:43:46 -0600 Subject: [PATCH 68/68] Update marketing version to 2.7.7 --- 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 3b214404..0b1dec82 100644 --- a/Meshtastic.xcodeproj/project.pbxproj +++ b/Meshtastic.xcodeproj/project.pbxproj @@ -2177,7 +2177,7 @@ "@executable_path/Frameworks", ); MACOSX_DEPLOYMENT_TARGET = 14.6; - MARKETING_VERSION = 2.7.5; + MARKETING_VERSION = 2.7.7; PRODUCT_BUNDLE_IDENTIFIER = gvh.MeshtasticClient; PRODUCT_NAME = "$(TARGET_NAME)"; SUPPORTS_MACCATALYST = YES; @@ -2212,7 +2212,7 @@ "@executable_path/Frameworks", ); MACOSX_DEPLOYMENT_TARGET = 14.6; - MARKETING_VERSION = 2.7.5; + MARKETING_VERSION = 2.7.7; PRODUCT_BUNDLE_IDENTIFIER = gvh.MeshtasticClient; PRODUCT_NAME = "$(TARGET_NAME)"; SUPPORTS_MACCATALYST = YES; @@ -2244,7 +2244,7 @@ "@executable_path/../../Frameworks", ); MACOSX_DEPLOYMENT_TARGET = 14.6; - MARKETING_VERSION = 2.7.5; + MARKETING_VERSION = 2.7.7; PRODUCT_BUNDLE_IDENTIFIER = gvh.MeshtasticClient.Widgets; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; @@ -2277,7 +2277,7 @@ "@executable_path/../../Frameworks", ); MACOSX_DEPLOYMENT_TARGET = 14.6; - MARKETING_VERSION = 2.7.5; + MARKETING_VERSION = 2.7.7; PRODUCT_BUNDLE_IDENTIFIER = gvh.MeshtasticClient.Widgets; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = "";