From f65fb5890c06fefd7d72780595fca1003f16be66 Mon Sep 17 00:00:00 2001 From: Benjamin Faershtein <119711889+RCGV1@users.noreply.github.com> Date: Tue, 1 Apr 2025 22:36:37 -0700 Subject: [PATCH 001/213] Add animation to demo disconnect --- Meshtastic/Views/Bluetooth/Connect.swift | 43 ++++++++++++++++++++++++ 1 file changed, 43 insertions(+) diff --git a/Meshtastic/Views/Bluetooth/Connect.swift b/Meshtastic/Views/Bluetooth/Connect.swift index 880faf8d..ce12f5d6 100644 --- a/Meshtastic/Views/Bluetooth/Connect.swift +++ b/Meshtastic/Views/Bluetooth/Connect.swift @@ -26,6 +26,10 @@ struct Connect: View { @State var liveActivityStarted = false @State var presentingSwitchPreferredPeripheral = false @State var selectedPeripherialId = "" + @State private var showSwipeDemo = false + @State private var swipeDemoOffset: CGFloat = 0 + @State private var showDeleteButton: Bool = false + @AppStorage("hasSeenSwipeDemo") private var hasSeenSwipeDemo = false init () { let notificationCenter = UNUserNotificationCenter.current() @@ -89,6 +93,28 @@ struct Connect: View { .font(.caption) .foregroundColor(Color.gray) .padding([.top]) + .offset(x: swipeDemoOffset) + .overlay( + GeometryReader { geometry in + ZStack { + Rectangle() + .foregroundColor(.red) + .frame(width: 80) + .offset(x: geometry.size.width - 80) + VStack { + Image(systemName: "antenna.radiowaves.left.and.right.slash") + .foregroundColor(.white) + .font(.title3) + Text("Disconnect") + .foregroundColor(.white) + .font(.caption) + } + } + .offset(x: geometry.size.width - 50) + + .opacity(showDeleteButton ? 1 : 0) + } + ) .swipeActions { Button(role: .destructive) { if let connectedPeripheral = bleManager.connectedPeripheral, @@ -323,6 +349,23 @@ struct Connect: View { } else { isUnsetRegion = false } + // Show swipe demo if user hasn't seen it before and we're connected + if !hasSeenSwipeDemo && bleManager.isSubscribed && node != nil { + DispatchQueue.main.asyncAfter(deadline: .now() + 1.0) { + withAnimation(.easeInOut(duration: 0.6)) { + swipeDemoOffset = -80 + showDeleteButton = true + } + DispatchQueue.main.asyncAfter(deadline: .now() + 1.5) { + withAnimation(.easeInOut(duration: 0.6)) { + swipeDemoOffset = 0 + showDeleteButton = false + } + // Mark as seen so it doesn't appear again + hasSeenSwipeDemo = true + } + } + } } catch { Logger.data.error("💥 Error fetching node info: \(error.localizedDescription, privacy: .public)") } From 927dc119483f5eae9426608754b2a92bae481d24 Mon Sep 17 00:00:00 2001 From: Benjamin Faershtein <119711889+RCGV1@users.noreply.github.com> Date: Wed, 2 Apr 2025 14:51:23 -0700 Subject: [PATCH 002/213] Added disconnect to long press context menu --- Meshtastic/Views/Bluetooth/Connect.swift | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/Meshtastic/Views/Bluetooth/Connect.swift b/Meshtastic/Views/Bluetooth/Connect.swift index ce12f5d6..cd79df99 100644 --- a/Meshtastic/Views/Bluetooth/Connect.swift +++ b/Meshtastic/Views/Bluetooth/Connect.swift @@ -149,7 +149,15 @@ struct Connect: View { Text("Short Name: \(node?.user?.shortName ?? "?")") Text("Long Name: \(node?.user?.longName?.addingVariationSelectors ?? "unknown".localized)") Text("BLE RSSI: \(connectedPeripheral.rssi)") - + + Button(role: .destructive) { + if let connectedPeripheral = bleManager.connectedPeripheral, + connectedPeripheral.peripheral.state == .connected { + bleManager.disconnectPeripheral(reconnect: false) + } + } label: { + Label("Disconnect", systemImage: "antenna.radiowaves.left.and.right.slash") + } Button { if !bleManager.sendShutdown(fromUser: node!.user!, toUser: node!.user!, adminIndex: node!.myInfo!.adminIndex) { Logger.mesh.error("Shutdown Failed") From 695147fcf62564d23daf907bfb0314b7be3b0199 Mon Sep 17 00:00:00 2001 From: Benjamin Faershtein <119711889+RCGV1@users.noreply.github.com> Date: Wed, 2 Apr 2025 18:02:27 -0700 Subject: [PATCH 003/213] Aded App Intent for disconnect node --- Localizable.xcstrings | 7 +++++ Meshtastic.xcodeproj/project.pbxproj | 4 +++ .../AppIntents/DisconnectNodeIntent.swift | 31 +++++++++++++++++++ Meshtastic/AppIntents/ShortcutsProvider.swift | 8 +++++ Meshtastic/Helpers/BLEManager.swift | 31 +++++++++++-------- 5 files changed, 68 insertions(+), 13 deletions(-) create mode 100644 Meshtastic/AppIntents/DisconnectNodeIntent.swift diff --git a/Localizable.xcstrings b/Localizable.xcstrings index 08f7b2ae..307e9e69 100644 --- a/Localizable.xcstrings +++ b/Localizable.xcstrings @@ -8691,6 +8691,12 @@ } } } + }, + "Disconnect Node" : { + + }, + "Disconnect the currently connected node" : { + }, "Dismiss" : { "localizations" : { @@ -23168,6 +23174,7 @@ } }, "Power Metrics Log}" : { + "extractionState" : "stale", "localizations" : { "sr" : { "stringUnit" : { diff --git a/Meshtastic.xcodeproj/project.pbxproj b/Meshtastic.xcodeproj/project.pbxproj index 58e2baa5..2d6ac2c6 100644 --- a/Meshtastic.xcodeproj/project.pbxproj +++ b/Meshtastic.xcodeproj/project.pbxproj @@ -62,6 +62,7 @@ BCB613832C672A2600485544 /* MessageChannelIntent.swift in Sources */ = {isa = PBXBuildFile; fileRef = BCB613822C672A2600485544 /* MessageChannelIntent.swift */; }; BCB613852C68703800485544 /* NodePositionIntent.swift in Sources */ = {isa = PBXBuildFile; fileRef = BCB613842C68703800485544 /* NodePositionIntent.swift */; }; BCB613872C69A0FB00485544 /* AppIntentErrors.swift in Sources */ = {isa = PBXBuildFile; fileRef = BCB613862C69A0FB00485544 /* AppIntentErrors.swift */; }; + BCD93CBA2D9E11A2006C9214 /* DisconnectNodeIntent.swift in Sources */ = {isa = PBXBuildFile; fileRef = BCD93CB92D9E11A2006C9214 /* DisconnectNodeIntent.swift */; }; BCE2D3C32C7ADF42008E6199 /* ShutDownNodeIntent.swift in Sources */ = {isa = PBXBuildFile; fileRef = BCE2D3C22C7ADF42008E6199 /* ShutDownNodeIntent.swift */; }; BCE2D3C52C7AE369008E6199 /* RestartNodeIntent.swift in Sources */ = {isa = PBXBuildFile; fileRef = BCE2D3C42C7AE369008E6199 /* RestartNodeIntent.swift */; }; BCE2D3C72C7B0D0A008E6199 /* ShortcutsProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = BCE2D3C62C7B0D0A008E6199 /* ShortcutsProvider.swift */; }; @@ -326,6 +327,7 @@ BCB613822C672A2600485544 /* MessageChannelIntent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageChannelIntent.swift; sourceTree = ""; }; BCB613842C68703800485544 /* NodePositionIntent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NodePositionIntent.swift; sourceTree = ""; }; BCB613862C69A0FB00485544 /* AppIntentErrors.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppIntentErrors.swift; sourceTree = ""; }; + BCD93CB92D9E11A2006C9214 /* DisconnectNodeIntent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DisconnectNodeIntent.swift; sourceTree = ""; }; BCE2D3C22C7ADF42008E6199 /* ShutDownNodeIntent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShutDownNodeIntent.swift; sourceTree = ""; }; BCE2D3C42C7AE369008E6199 /* RestartNodeIntent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RestartNodeIntent.swift; sourceTree = ""; }; BCE2D3C62C7B0D0A008E6199 /* ShortcutsProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShortcutsProvider.swift; sourceTree = ""; }; @@ -686,6 +688,7 @@ BCE2D3C62C7B0D0A008E6199 /* ShortcutsProvider.swift */, BC6B45FE2CB2F98900723CEB /* SaveChannelSettingsIntent.swift */, BC47C2EE2CE0017D008245CA /* MessageNodeIntent.swift */, + BCD93CB92D9E11A2006C9214 /* DisconnectNodeIntent.swift */, ); path = AppIntents; sourceTree = ""; @@ -1421,6 +1424,7 @@ DDC94FCE29CF55310082EA6E /* RtttlConfig.swift in Sources */, DD964FBD296E6B01007C176F /* EmojiOnlyTextField.swift in Sources */, DD8169FF272476C700F4AB02 /* LogDocument.swift in Sources */, + BCD93CBA2D9E11A2006C9214 /* DisconnectNodeIntent.swift in Sources */, DDC94FC129CE063B0082EA6E /* BatteryLevel.swift in Sources */, 231B3F252D087C3C0069A07D /* EnvironmentDefaultColumns.swift in Sources */, 25F5D5BE2C3F6D87008036E3 /* NavigationState.swift in Sources */, diff --git a/Meshtastic/AppIntents/DisconnectNodeIntent.swift b/Meshtastic/AppIntents/DisconnectNodeIntent.swift new file mode 100644 index 00000000..2e9986d0 --- /dev/null +++ b/Meshtastic/AppIntents/DisconnectNodeIntent.swift @@ -0,0 +1,31 @@ +// +// DisconnectNodeIntent.swift +// Meshtastic +// +// Created by Benjamin Faershtein on 4/2/25. +// + +import Foundation +import AppIntents + +struct DisconnectNodeIntent: AppIntent { + static var title: LocalizedStringResource = "Disconnect Node" + + static var description: IntentDescription = "Disconnect the currently connected node" + + + func perform() async throws -> some IntentResult { + if !BLEManager.shared.isConnected { + throw AppIntentErrors.AppIntentError.notConnected + } + + if let connectedPeripheral = BLEManager.shared.connectedPeripheral, + connectedPeripheral.peripheral.state == .connected { + BLEManager.shared.disconnectPeripheral(reconnect: false) + } else { + throw AppIntentErrors.AppIntentError.message("Error disconnecting node") + } + + return .result() + } +} diff --git a/Meshtastic/AppIntents/ShortcutsProvider.swift b/Meshtastic/AppIntents/ShortcutsProvider.swift index b21c7e7d..fcc9ffec 100644 --- a/Meshtastic/AppIntents/ShortcutsProvider.swift +++ b/Meshtastic/AppIntents/ShortcutsProvider.swift @@ -32,5 +32,13 @@ struct ShortcutsProvider: AppShortcutsProvider { "Send a \(.applicationName) group message"], shortTitle: "Group Message", systemImageName: "message") + + AppShortcut(intent: DisconnectNodeIntent(), + phrases: ["Disconnect \(.applicationName) node", + "Disconnect my \(.applicationName) node", + "Disconnect from \(.applicationName)", + "Disconnect \(.applicationName)"], + shortTitle: "Disconnect", + systemImageName: "antenna.radiowaves.left.and.right.slash") } } diff --git a/Meshtastic/Helpers/BLEManager.swift b/Meshtastic/Helpers/BLEManager.swift index 9f633f30..e4e9ae73 100644 --- a/Meshtastic/Helpers/BLEManager.swift +++ b/Meshtastic/Helpers/BLEManager.swift @@ -177,20 +177,25 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate // Disconnect Connected Peripheral func disconnectPeripheral(reconnect: Bool = true) { - - guard let connectedPeripheral = connectedPeripheral else { return } - if mqttProxyConnected { - mqttManager.mqttClientProxy?.disconnect() + // Ensure all operations run on the main thread + DispatchQueue.main.async { [weak self] in + guard let self = self else { return } + guard let connectedPeripheral = self.connectedPeripheral else { return } + + if self.mqttProxyConnected { + self.mqttManager.mqttClientProxy?.disconnect() + } + + self.automaticallyReconnect = reconnect + self.centralManager?.cancelPeripheralConnection(connectedPeripheral.peripheral) + self.FROMRADIO_characteristic = nil + self.isConnected = false + self.isSubscribed = false + self.invalidVersion = false + self.connectedVersion = "0.0.0" + self.stopScanning() + self.startScanning() } - automaticallyReconnect = reconnect - centralManager?.cancelPeripheralConnection(connectedPeripheral.peripheral) - FROMRADIO_characteristic = nil - isConnected = false - isSubscribed = false - invalidVersion = false - connectedVersion = "0.0.0" - stopScanning() - startScanning() } // Called each time a peripheral is connected From 8293dafae0fb626bc5a99d3070f58c0cd124f67a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nikola=20Da=C5=A1i=C4=87?= Date: Sun, 6 Apr 2025 09:28:57 +0200 Subject: [PATCH 004/213] Serbian translations sync --- Localizable.xcstrings | 127 +++++++++++++----------------------------- 1 file changed, 40 insertions(+), 87 deletions(-) diff --git a/Localizable.xcstrings b/Localizable.xcstrings index 6c2731d1..dd215188 100644 --- a/Localizable.xcstrings +++ b/Localizable.xcstrings @@ -37,7 +37,14 @@ } }, " %@%%" : { - + "localizations" : { + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : " %@%%" + } + } + } }, ": %@" : { "localizations" : { @@ -3608,7 +3615,14 @@ } }, "Channel Utilization %@%%" : { - + "localizations" : { + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Искоришћеност канала %@%%" + } + } + } }, "channel.role.disabled" : { "extractionState" : "migrated", @@ -12138,23 +12152,6 @@ } } }, - "HUMIDITY" : { - "extractionState" : "stale", - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "LUFTFEUCHTIGKEIT" - } - }, - "sr" : { - "stringUnit" : { - "state" : "translated", - "value" : "ВЛАЖНОСТ" - } - } - } - }, "hybrid" : { "extractionState" : "migrated", "localizations" : { @@ -17140,71 +17137,6 @@ } } }, - "mesh.log" : { - "extractionState" : "stale", - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Mesh Log" - } - }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Mesh Log" - } - }, - "fr" : { - "stringUnit" : { - "state" : "translated", - "value" : "Journal du maillage" - } - }, - "he" : { - "stringUnit" : { - "state" : "translated", - "value" : "יומן מש" - } - }, - "pl" : { - "stringUnit" : { - "state" : "translated", - "value" : "Dziennik Sieci" - } - }, - "pt-PT" : { - "stringUnit" : { - "state" : "translated", - "value" : "Log do Mesh" - } - }, - "se" : { - "stringUnit" : { - "state" : "translated", - "value" : "Mesh-logg" - } - }, - "sr" : { - "stringUnit" : { - "state" : "translated", - "value" : "Логови мреже" - } - }, - "zh-Hans" : { - "stringUnit" : { - "state" : "translated", - "value" : "Mesh 日志" - } - }, - "zh-Hant-TW" : { - "stringUnit" : { - "state" : "translated", - "value" : "Mesh 紀錄檔" - } - } - } - }, "mesh.log.ambientlighting.config %@" : { "extractionState" : "migrated", "localizations" : { @@ -23462,7 +23394,14 @@ } }, "Pressure" : { - + "localizations" : { + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Притисак" + } + } + } }, "Primary" : { "localizations" : { @@ -32527,7 +32466,14 @@ } }, "Volts %@" : { - + "localizations" : { + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Волти %@" + } + } + } }, "Waiting" : { "localizations" : { @@ -32780,7 +32726,14 @@ } }, "Wind" : { - + "localizations" : { + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ветар" + } + } + } }, "Wind Direction" : { "localizations" : { From 65239b39fe95421bda1edf1fd408bd863659e48c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nikola=20Da=C5=A1i=C4=87?= Date: Sun, 6 Apr 2025 09:45:31 +0200 Subject: [PATCH 005/213] Sync Serbian translations --- Localizable.xcstrings | 195 +++++++++++++++--- Meshtastic/Tips/BluetoothTips.swift | 2 +- .../Config/Module/StoreForwardConfig.swift | 2 +- 3 files changed, 170 insertions(+), 29 deletions(-) diff --git a/Localizable.xcstrings b/Localizable.xcstrings index d69614a3..03836b7c 100644 --- a/Localizable.xcstrings +++ b/Localizable.xcstrings @@ -4267,7 +4267,14 @@ } }, "Community Support" : { - + "localizations" : { + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Подршка заједнице" + } + } + } }, "Config" : { "localizations" : { @@ -5631,7 +5638,14 @@ } }, "Confirm" : { - + "localizations" : { + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Потврди" + } + } + } }, "Connect to a Node" : { "localizations" : { @@ -5650,7 +5664,14 @@ } }, "Connect to new radio?" : { - + "localizations" : { + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Повезати се на нови радио?" + } + } + } }, "connected" : { "localizations" : { @@ -5731,9 +5752,6 @@ } } } - }, - "Connected Radio" : { - }, "connected.radio" : { "localizations" : { @@ -5864,7 +5882,14 @@ } }, "Connecting to a new radio will clear all app data on the phone." : { - + "localizations" : { + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Повезивање са новим радио уређајем ће очистити све локалне податке апликације на телефону." + } + } + } }, "Connection Attempt %lld of 10" : { "localizations" : { @@ -6378,7 +6403,14 @@ } }, "Currently showing modules that may not be supported by this node." : { - + "localizations" : { + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Тренутно се приказују модули које овај чвор можда не подржава." + } + } + } }, "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" : { @@ -9315,7 +9347,14 @@ } }, "Enable broadcasting packets via UDP over the local network." : { - + "localizations" : { + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Омогућите емитовање пакета путем UDP-а преко локалне мреже." + } + } + } }, "Enable Notifications" : { "localizations" : { @@ -9334,7 +9373,14 @@ } }, "Enable this device as a Store and Forward server. Requires an ESP32 device with PSRAM." : { - + "localizations" : { + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Омогућите овај уређај као сервер за складиштење и прослеђивање. Захтева ESP32 уређај са PSRAM-ом." + } + } + } }, "enabled" : { "localizations" : { @@ -9433,7 +9479,14 @@ } }, "Enables the store and forward module." : { - + "localizations" : { + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Омогућава модул за складиштење и прослеђивање." + } + } + } }, "Enabling Ethernet will disable the bluetooth connection to the app." : { "localizations" : { @@ -10691,7 +10744,14 @@ } }, "Full Support" : { - + "localizations" : { + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Потпуна подршка" + } + } + } }, "gas" : { "extractionState" : "manual", @@ -22145,7 +22205,14 @@ } }, "OTA Updates are not supported on this NRF Device." : { - + "localizations" : { + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "OTA ажурирања нису подржана на овом NRF уређају." + } + } + } }, "OTA Updates are not supported on your platform." : { "localizations" : { @@ -23533,7 +23600,14 @@ } }, "Radiation" : { - + "localizations" : { + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Радијација" + } + } + } }, "Radio Disconnected" : { "extractionState" : "manual", @@ -27032,7 +27106,14 @@ } }, "Send a heartbeat to advertise the server's presence." : { - + "localizations" : { + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Пошаљите откуцај срца да бисте рекламирали присуство сервера." + } + } + } }, "Send a message to a certain meshtastic channel" : { "localizations" : { @@ -27784,7 +27865,14 @@ } }, "Server Option" : { - + "localizations" : { + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Опција сервера" + } + } + } }, "Set" : { "localizations" : { @@ -27943,9 +28031,6 @@ } } } - }, - "Settings" : { - }, "Share QR Code & Link" : { "localizations" : { @@ -28300,7 +28385,14 @@ } }, "Shows information for the Lora radio connected via bluetooth. You can swipe left to disconnect the radio and long press start the live activity." : { - + "localizations" : { + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Приказује информације за Lora радио повезан преко Bluetooth-а. Можете превући улево да бисте прекинули везу са радиом, а дугим притиском покрећете „уживо“ активност." + } + } + } }, "Shut Down" : { "localizations" : { @@ -28418,10 +28510,24 @@ } }, "Soil Moisture" : { - + "localizations" : { + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Влага земљишта" + } + } + } }, "Soil Temp" : { - + "localizations" : { + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Температура земљиша" + } + } + } }, "Specifies how long the monitored GPIO should output." : { "localizations" : { @@ -28792,7 +28898,14 @@ } }, "Store and forward servers require an ESP32 device with PSRAM or Linux Native." : { - + "localizations" : { + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Сервери за складиштење и прослеђивање захтевају ESP32 уређај са PSRAM-ом или Линук native-ом." + } + } + } }, "storeforward.heartbeat" : { "localizations" : { @@ -30074,7 +30187,14 @@ } }, "The Router roles are designed for high vantage locations like mountaintops and towers. This node needs to be able to have a good direct connection to most of the nodes on the network or else this will significantly hurt the network." : { - + "localizations" : { + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Улоге рутера су дизајниране за локације са високим погледом као што су врхови планина и куле. Овај чвор мора бити у могућности да има добру директну везу са већином чворова на мрежи или ће то значајно наштетити мрежи." + } + } + } }, "The secondary public key authorized to send admin messages to this node." : { "localizations" : { @@ -30250,7 +30370,14 @@ } }, "This node does not support any configurable modules." : { - + "localizations" : { + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Овај чвор не подржава ниједан конфигурабилан модул." + } + } + } }, "This will disable fixed position and remove the currently set position." : { "localizations" : { @@ -31275,7 +31402,14 @@ } }, "UDP Broadcast" : { - + "localizations" : { + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "UDP емитовање" + } + } + } }, "Ukraine 433mhz" : { "extractionState" : "manual", @@ -32458,7 +32592,14 @@ } }, "Weight" : { - + "localizations" : { + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Тежина" + } + } + } }, "What does the lock mean?" : { "localizations" : { diff --git a/Meshtastic/Tips/BluetoothTips.swift b/Meshtastic/Tips/BluetoothTips.swift index 838d29fc..a10f15d7 100644 --- a/Meshtastic/Tips/BluetoothTips.swift +++ b/Meshtastic/Tips/BluetoothTips.swift @@ -13,7 +13,7 @@ struct BluetoothConnectionTip: Tip { return "tip.bluetooth.connect" } var title: Text { - Text("Connected Radio") + Text("connected.radio") } var message: Text? { Text("Shows information for the Lora radio connected via bluetooth. You can swipe left to disconnect the radio and long press start the live activity.") diff --git a/Meshtastic/Views/Settings/Config/Module/StoreForwardConfig.swift b/Meshtastic/Views/Settings/Config/Module/StoreForwardConfig.swift index fe6abcd1..0859f87f 100644 --- a/Meshtastic/Views/Settings/Config/Module/StoreForwardConfig.swift +++ b/Meshtastic/Views/Settings/Config/Module/StoreForwardConfig.swift @@ -44,7 +44,7 @@ struct StoreForwardConfig: View { } if enabled { - Section(header: Text("Settings")) { + Section(header: Text("settings")) { Toggle(isOn: $heartbeat) { Label("storeforward.heartbeat", systemImage: "waveform.path.ecg") Text("Send a heartbeat to advertise the server's presence.") From 531e74dd5cf921f7debc8aa4d46a832b114a53e3 Mon Sep 17 00:00:00 2001 From: Benjamin Faershtein <119711889+RCGV1@users.noreply.github.com> Date: Tue, 6 May 2025 21:49:44 -0700 Subject: [PATCH 006/213] Fixed waypoint updating logic and waypoint notification --- Meshtastic/Helpers/MeshPackets.swift | 88 ++++++++++++++++++---------- 1 file changed, 57 insertions(+), 31 deletions(-) diff --git a/Meshtastic/Helpers/MeshPackets.swift b/Meshtastic/Helpers/MeshPackets.swift index bcc4659b..e25ae8c1 100644 --- a/Meshtastic/Helpers/MeshPackets.swift +++ b/Meshtastic/Helpers/MeshPackets.swift @@ -1035,22 +1035,33 @@ func textMessageAppPacket( } } -func waypointPacket (packet: MeshPacket, context: NSManagedObjectContext) { - +func waypointPacket(packet: MeshPacket, context: NSManagedObjectContext) { let logString = String.localizedStringWithFormat("mesh.log.waypoint.received %@".localized, String(packet.from)) Logger.mesh.info("📍 \(logString, privacy: .public)") - let fetchWaypointRequest = WaypointEntity.fetchRequest() - fetchWaypointRequest.predicate = NSPredicate(format: "id == %lld", Int64(packet.id)) - do { - if let waypointMessage = try? Waypoint(serializedBytes: packet.decoded.payload) { - let fetchedWaypoint = try context.fetch(fetchWaypointRequest) - if fetchedWaypoint.isEmpty { - let waypoint = WaypointEntity(context: context) + // Fetch waypoint by waypointMessage.id, not packet.id + let fetchWaypointRequest = WaypointEntity.fetchRequest() + fetchWaypointRequest.predicate = NSPredicate(format: "id == %lld", Int64(waypointMessage.id)) - waypoint.id = Int64(packet.id) + let fetchedWaypoint = try context.fetch(fetchWaypointRequest) + // Fetch the node info to get the short name + var nodeShortName: String = "?" + let fetchNodeRequest = NodeInfoEntity.fetchRequest() + fetchNodeRequest.predicate = NSPredicate(format: "num == %lld", Int64(packet.from)) + do { + let fetchedNode = try context.fetch(fetchNodeRequest) + if let node = fetchedNode.first, let user = node.user { + nodeShortName = user.shortName ?? node.user?.userId ?? String(packet.from.toHex()) + } + } catch { + Logger.data.error("Failed to fetch NodeInfoEntity for node \(packet.from.toHex(), privacy: .public): \(error)") + } + if fetchedWaypoint.isEmpty { + // Create a new waypoint + let waypoint = WaypointEntity(context: context) + waypoint.id = Int64(waypointMessage.id) // Use waypointMessage.id waypoint.name = waypointMessage.name waypoint.longDescription = waypointMessage.description_p waypoint.latitudeI = waypointMessage.latitudeI @@ -1073,7 +1084,7 @@ func waypointPacket (packet: MeshPacket, context: NSManagedObjectContext) { manager.notifications = [ Notification( id: ("notification.id.\(waypoint.id)"), - title: "New Waypoint Received", + title: "New Waypoint From \(nodeShortName)", subtitle: "\(icon) \(waypoint.name ?? "Dropped Pin")", content: "\(waypoint.longDescription ?? "\(latitude), \(longitude)")", target: "map", @@ -1088,26 +1099,41 @@ func waypointPacket (packet: MeshPacket, context: NSManagedObjectContext) { Logger.data.error("Error Saving WaypointEntity from WAYPOINT_APP \(nsError, privacy: .public)") } } else { - fetchedWaypoint[0].id = Int64(packet.id) - fetchedWaypoint[0].name = waypointMessage.name - fetchedWaypoint[0].longDescription = waypointMessage.description_p - fetchedWaypoint[0].latitudeI = waypointMessage.latitudeI - fetchedWaypoint[0].longitudeI = waypointMessage.longitudeI - fetchedWaypoint[0].icon = Int64(waypointMessage.icon) - fetchedWaypoint[0].locked = Int64(waypointMessage.lockedTo) - if waypointMessage.expire >= 1 { - fetchedWaypoint[0].expire = Date(timeIntervalSince1970: TimeInterval(Int64(waypointMessage.expire))) - } else { - fetchedWaypoint[0].expire = nil - } - fetchedWaypoint[0].lastUpdated = Date() - do { - try context.save() - Logger.data.info("💾 Updated Node Waypoint App Packet For: \(fetchedWaypoint[0].id, privacy: .public)") - } catch { - context.rollback() - let nsError = error as NSError - Logger.data.error("Error Saving WaypointEntity from WAYPOINT_APP \(nsError, privacy: .public)") + // Update existing waypoint + let existingWaypoint = fetchedWaypoint[0] + if existingWaypoint.locked == 0 || existingWaypoint.locked == packet.from { + let currentTime = Int64(Date().timeIntervalSince1970) + if waypointMessage.expire > 0 && waypointMessage.expire <= currentTime { + BLEManager.shared.context.delete(existingWaypoint) + do { + try context.save() + Logger.data.info("💾 Deleted a waypoint") + } catch { + context.rollback() + let nsError = error as NSError + Logger.data.error("Error Saving WaypointEntity from WAYPOINT_APP \(nsError, privacy: .public)") + } + } + existingWaypoint.name = waypointMessage.name + existingWaypoint.longDescription = waypointMessage.description_p + existingWaypoint.latitudeI = waypointMessage.latitudeI + existingWaypoint.longitudeI = waypointMessage.longitudeI + existingWaypoint.icon = Int64(waypointMessage.icon) + existingWaypoint.locked = Int64(waypointMessage.lockedTo) + if waypointMessage.expire >= 1 { + existingWaypoint.expire = Date(timeIntervalSince1970: TimeInterval(Int64(waypointMessage.expire))) + } else { + existingWaypoint.expire = nil + } + existingWaypoint.lastUpdated = Date() + do { + try context.save() + Logger.data.info("💾 Updated Node Waypoint App Packet For: \(existingWaypoint.id, privacy: .public)") + } catch { + context.rollback() + let nsError = error as NSError + Logger.data.error("Error Saving WaypointEntity from WAYPOINT_APP \(nsError, privacy: .public)") + } } } } From a1d5a8a0d04c62a71e10d12761e065281ad3c3b1 Mon Sep 17 00:00:00 2001 From: Benjamin Faershtein <119711889+RCGV1@users.noreply.github.com> Date: Tue, 6 May 2025 22:03:24 -0700 Subject: [PATCH 007/213] fixed a small issue where it tries updating it after its deleted --- Meshtastic/Helpers/MeshPackets.swift | 41 ++++++++++++++-------------- 1 file changed, 21 insertions(+), 20 deletions(-) diff --git a/Meshtastic/Helpers/MeshPackets.swift b/Meshtastic/Helpers/MeshPackets.swift index e25ae8c1..870c13cb 100644 --- a/Meshtastic/Helpers/MeshPackets.swift +++ b/Meshtastic/Helpers/MeshPackets.swift @@ -1104,7 +1104,7 @@ func waypointPacket(packet: MeshPacket, context: NSManagedObjectContext) { if existingWaypoint.locked == 0 || existingWaypoint.locked == packet.from { let currentTime = Int64(Date().timeIntervalSince1970) if waypointMessage.expire > 0 && waypointMessage.expire <= currentTime { - BLEManager.shared.context.delete(existingWaypoint) + context.delete(existingWaypoint) do { try context.save() Logger.data.info("💾 Deleted a waypoint") @@ -1113,26 +1113,27 @@ func waypointPacket(packet: MeshPacket, context: NSManagedObjectContext) { let nsError = error as NSError Logger.data.error("Error Saving WaypointEntity from WAYPOINT_APP \(nsError, privacy: .public)") } - } - existingWaypoint.name = waypointMessage.name - existingWaypoint.longDescription = waypointMessage.description_p - existingWaypoint.latitudeI = waypointMessage.latitudeI - existingWaypoint.longitudeI = waypointMessage.longitudeI - existingWaypoint.icon = Int64(waypointMessage.icon) - existingWaypoint.locked = Int64(waypointMessage.lockedTo) - if waypointMessage.expire >= 1 { - existingWaypoint.expire = Date(timeIntervalSince1970: TimeInterval(Int64(waypointMessage.expire))) } else { - existingWaypoint.expire = nil - } - existingWaypoint.lastUpdated = Date() - do { - try context.save() - Logger.data.info("💾 Updated Node Waypoint App Packet For: \(existingWaypoint.id, privacy: .public)") - } catch { - context.rollback() - let nsError = error as NSError - Logger.data.error("Error Saving WaypointEntity from WAYPOINT_APP \(nsError, privacy: .public)") + existingWaypoint.name = waypointMessage.name + existingWaypoint.longDescription = waypointMessage.description_p + existingWaypoint.latitudeI = waypointMessage.latitudeI + existingWaypoint.longitudeI = waypointMessage.longitudeI + existingWaypoint.icon = Int64(waypointMessage.icon) + existingWaypoint.locked = Int64(waypointMessage.lockedTo) + if waypointMessage.expire >= 1 { + existingWaypoint.expire = Date(timeIntervalSince1970: TimeInterval(Int64(waypointMessage.expire))) + } else { + existingWaypoint.expire = nil + } + existingWaypoint.lastUpdated = Date() + do { + try context.save() + Logger.data.info("💾 Updated Node Waypoint App Packet For: \(existingWaypoint.id, privacy: .public)") + } catch { + context.rollback() + let nsError = error as NSError + Logger.data.error("Error Saving WaypointEntity from WAYPOINT_APP \(nsError, privacy: .public)") + } } } } From 1a2429f36f1fac66e4c2c4a6b5f780b2c597ec77 Mon Sep 17 00:00:00 2001 From: Benjamin Faershtein <119711889+RCGV1@users.noreply.github.com> Date: Wed, 7 May 2025 19:43:47 -0700 Subject: [PATCH 008/213] Add fallback to lastKnown location to fix the "AppleParkBug" --- Meshtastic/Helpers/LocationsHandler.swift | 35 +++++++++++++++++++++-- 1 file changed, 32 insertions(+), 3 deletions(-) diff --git a/Meshtastic/Helpers/LocationsHandler.swift b/Meshtastic/Helpers/LocationsHandler.swift index 75830805..f7166f70 100644 --- a/Meshtastic/Helpers/LocationsHandler.swift +++ b/Meshtastic/Helpers/LocationsHandler.swift @@ -8,6 +8,7 @@ import SwiftUI import CoreLocation import OSLog +import CoreData // Shared state that manages the `CLLocationManager` and `CLBackgroundActivitySession`. @MainActor class LocationsHandler: ObservableObject { @@ -109,15 +110,43 @@ import OSLog } else { locationsArray = [location] } + UserDefaults.standard.set(location.coordinate.latitude, forKey: "lastKnownLatitude") + UserDefaults.standard.set(location.coordinate.longitude, forKey: "lastKnownLongitude") + UserDefaults.standard.set(Date().timeIntervalSince1970, forKey: "lastKnownLocationTimestamp") return true } - static let DefaultLocation = CLLocationCoordinate2D(latitude: 37.3346, longitude: -122.0090) static var currentLocation: CLLocationCoordinate2D { - guard let location = shared.manager.location else { + if let location = shared.manager.location { + return location.coordinate + } else { + // Check authorization status + let status = shared.manager.authorizationStatus + switch status { + case .notDetermined: + Logger.services.info("📍 [App] Location permission not determined, requesting authorization") + shared.manager.requestWhenInUseAuthorization() + case .denied, .restricted: + Logger.services.warning("📍 [App] Location access denied or restricted. Please enable location services in Settings to get accurate positioning!") + shared.manager.requestWhenInUseAuthorization() + default: + break + } + // Fallback 1: Last known location from UserDefaults (if within 4 hours) + if let lat = UserDefaults.standard.object(forKey: "lastKnownLatitude") as? Double, + let lon = UserDefaults.standard.object(forKey: "lastKnownLongitude") as? Double, + let timestamp = UserDefaults.standard.object(forKey: "lastKnownLocationTimestamp") as? Double, + lat >= -90 && lat <= 90, + lon >= -180 && lon <= 180, + Date().timeIntervalSince1970 - timestamp <= 14_400 { // 4 hours in seconds + Logger.services.info("📍 [App] Falling back to last known location (age: \(Int(Date().timeIntervalSince1970 - timestamp)) seconds)") + return CLLocationCoordinate2D(latitude: lat, longitude: lon) + } + + // Fallback 2: Default location + Logger.services.warning("📍 [App] No Location and no last known location, something is really wrong. Teleporting user to Apple Park") return DefaultLocation } - return location.coordinate } static var satsInView: Int { From de05811f14d10bb808b46adc5d14eb6012b26f52 Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Thu, 8 May 2025 23:01:16 -0700 Subject: [PATCH 009/213] Fix fifteen second label --- Meshtastic/Enums/SerialConfigEnums.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Meshtastic/Enums/SerialConfigEnums.swift b/Meshtastic/Enums/SerialConfigEnums.swift index 681ef89e..131b3d26 100644 --- a/Meshtastic/Enums/SerialConfigEnums.swift +++ b/Meshtastic/Enums/SerialConfigEnums.swift @@ -174,7 +174,7 @@ enum SerialTimeoutIntervals: Int, CaseIterable, Identifiable { case .tenSeconds: return "Ten Seconds".localized case .fifteenSeconds: - return "Thirty Seconds".localized + return "Fifteen Seconds".localized case .thirtySeconds: return "Thirty Seconds".localized case .oneMinute: From 20845f77fa26857bcce0c663089b4bb5c55a1ffb Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Thu, 8 May 2025 23:03:06 -0700 Subject: [PATCH 010/213] Update Meshtastic/Persistence/UpdateCoreData.swift Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- Meshtastic/Persistence/UpdateCoreData.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Meshtastic/Persistence/UpdateCoreData.swift b/Meshtastic/Persistence/UpdateCoreData.swift index b1892fa9..00916edb 100644 --- a/Meshtastic/Persistence/UpdateCoreData.swift +++ b/Meshtastic/Persistence/UpdateCoreData.swift @@ -687,7 +687,7 @@ func upsertNetworkConfigPacket(config: Config.NetworkConfig, nodeNum: Int64, ses func upsertPositionConfigPacket(config: Config.PositionConfig, nodeNum: Int64, sessionPasskey: Data? = Data(), context: NSManagedObjectContext) { - let logString = String.localizedStringWithFormat("Positon config received: %@".localized, String(nodeNum)) + let logString = String.localizedStringWithFormat("Position config received: %@".localized, String(nodeNum)) Logger.data.info("🗺️ \(logString, privacy: .public)") let fetchNodeInfoRequest = NodeInfoEntity.fetchRequest() From dcfad05599770b02e7516abed39fd2c04d40e08b Mon Sep 17 00:00:00 2001 From: Jake-B Date: Fri, 9 May 2025 16:46:08 -0400 Subject: [PATCH 011/213] Rate limited traceroute btn and progress indicator --- Localizable.xcstrings | 3 + Meshtastic.xcodeproj/project.pbxproj | 4 + .../Contents.json | 12 ++ .../progress.ring.dashed.svg | 169 ++++++++++++++++++ .../Views/Helpers/RateLimitedButton.swift | 113 ++++++++++++ .../Helpers/Actions/TraceRouteButton.swift | 26 ++- Meshtastic/Views/Nodes/NodeList.swift | 19 +- 7 files changed, 323 insertions(+), 23 deletions(-) create mode 100644 Meshtastic/Assets.xcassets/progress.ring.dashed.symbolset/Contents.json create mode 100644 Meshtastic/Assets.xcassets/progress.ring.dashed.symbolset/progress.ring.dashed.svg create mode 100644 Meshtastic/Views/Helpers/RateLimitedButton.swift diff --git a/Localizable.xcstrings b/Localizable.xcstrings index 95f524d2..8b8f65a2 100644 --- a/Localizable.xcstrings +++ b/Localizable.xcstrings @@ -35986,6 +35986,9 @@ } } } + }, + "Trace Route (in %@s)" : { + }, "Trace Route Log" : { "localizations" : { diff --git a/Meshtastic.xcodeproj/project.pbxproj b/Meshtastic.xcodeproj/project.pbxproj index 39426e2f..96995a31 100644 --- a/Meshtastic.xcodeproj/project.pbxproj +++ b/Meshtastic.xcodeproj/project.pbxproj @@ -28,6 +28,7 @@ 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 */; }; + 237B46962DC8F1C100B22D99 /* RateLimitedButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 237B46952DC8F1C100B22D99 /* RateLimitedButton.swift */; }; 251926852C3BA97800249DF5 /* FavoriteNodeButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 251926842C3BA97800249DF5 /* FavoriteNodeButton.swift */; }; 251926872C3BAE2200249DF5 /* NodeAlertsButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 251926862C3BAE2200249DF5 /* NodeAlertsButton.swift */; }; 2519268A2C3BB1B200249DF5 /* ExchangePositionsButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 251926892C3BB1B200249DF5 /* ExchangePositionsButton.swift */; }; @@ -294,6 +295,7 @@ 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 = ""; }; + 237B46952DC8F1C100B22D99 /* RateLimitedButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RateLimitedButton.swift; sourceTree = ""; }; 251926842C3BA97800249DF5 /* FavoriteNodeButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FavoriteNodeButton.swift; sourceTree = ""; }; 251926862C3BAE2200249DF5 /* NodeAlertsButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NodeAlertsButton.swift; sourceTree = ""; }; 251926892C3BB1B200249DF5 /* ExchangePositionsButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExchangePositionsButton.swift; sourceTree = ""; }; @@ -1041,6 +1043,7 @@ DD5E523D298F5A7D00D21B61 /* Weather */, DD6F65712C6AB8EC0053C113 /* SecureInput.swift */, 8D3F8A3E2D44BB02009EAAA4 /* PowerMetrics.swift */, + 237B46952DC8F1C100B22D99 /* RateLimitedButton.swift */, ); path = Helpers; sourceTree = ""; @@ -1412,6 +1415,7 @@ DDDB445429F8AD1600EE2349 /* Data.swift in Sources */, DDDB26462AACC0B7003AFCB7 /* NodeInfoItem.swift in Sources */, DDE5B4042B2279A700FCDD05 /* TraceRouteLog.swift in Sources */, + 237B46962DC8F1C100B22D99 /* RateLimitedButton.swift in Sources */, DD6193792863875F00E59241 /* SerialConfig.swift in Sources */, DDDB263F2AABEE20003AFCB7 /* NodeList.swift in Sources */, DDD5BB0B2C285E45007E03CA /* LogDetail.swift in Sources */, diff --git a/Meshtastic/Assets.xcassets/progress.ring.dashed.symbolset/Contents.json b/Meshtastic/Assets.xcassets/progress.ring.dashed.symbolset/Contents.json new file mode 100644 index 00000000..7c46aedd --- /dev/null +++ b/Meshtastic/Assets.xcassets/progress.ring.dashed.symbolset/Contents.json @@ -0,0 +1,12 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + }, + "symbols" : [ + { + "filename" : "progress.ring.dashed.svg", + "idiom" : "universal" + } + ] +} diff --git a/Meshtastic/Assets.xcassets/progress.ring.dashed.symbolset/progress.ring.dashed.svg b/Meshtastic/Assets.xcassets/progress.ring.dashed.symbolset/progress.ring.dashed.svg new file mode 100644 index 00000000..5d91388c --- /dev/null +++ b/Meshtastic/Assets.xcassets/progress.ring.dashed.symbolset/progress.ring.dashed.svg @@ -0,0 +1,169 @@ + + + + + + + + + + Weight/Scale Variations + Ultralight + Thin + Light + Regular + Medium + Semibold + Bold + Heavy + Black + + + + + + + + + + + Design Variations + Symbols are supported in up to nine weights and three scales. + For optimal layout with text and other symbols, vertically align + symbols with the adjacent text. + + + + + + Margins + Leading and trailing margins on the left and right side of each symbol + can be adjusted by modifying the x-location of the margin guidelines. + Modifications are automatically applied proportionally to all + scales and weights. + + + + Exporting + Symbols should be outlined when exporting to ensure the + design is preserved when submitting to Xcode. + Template v.6.0 + Requires Xcode 16 or greater + Generated from progress.ring.dashed + Typeset at 100.0 points + Small + Medium + Large + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Meshtastic/Views/Helpers/RateLimitedButton.swift b/Meshtastic/Views/Helpers/RateLimitedButton.swift new file mode 100644 index 00000000..30c5d667 --- /dev/null +++ b/Meshtastic/Views/Helpers/RateLimitedButton.swift @@ -0,0 +1,113 @@ +// +// RateLimitCountdownView.swift +// Meshtastic +// +// Created by Jake Bordens on 5/5/25. +// + +import SwiftUI + +// This class provides a rate limited button. +// Provide a key to differentiate which action is rate-limited +// This allows you to keep different rate limits for different action +// Rate limits are stored in a RateLimitStorage singleton, but do not persist +public struct RateLimitedButton: View { + typealias Builder = ((percentComplete: Double, secondsRemaining: TimeInterval)?) -> Content + + let key: String + + @StateObject var storage = RateLimitStorage.shared + + let rateLimit: TimeInterval + let content: Builder + let action: () -> Void + + init(key: String, rateLimit: TimeInterval, action: @escaping () -> Void, @ViewBuilder label: @escaping Builder) { + self.key = key + self.rateLimit = rateLimit + self.content = label + self.action = action + } + + public var body: some View { + let percentRemaining = storage.rateLimitRemainingPercentage(forKey: key) + let secondsRemaining = storage.rateLimitSecondsRemaining(forKey: key) + if percentRemaining > 0.0 { + content((percentRemaining, secondsRemaining)) + } else { + Button { + storage.actionOccured(forKey: key, rateLimit: rateLimit) + action() + } label: { + content(nil) + } + } + } +} + +// To store the time an action occured (name by a key) and the time limit +// Does not persist across app launches +class RateLimitStorage: ObservableObject { + private struct RateLimiter { + var actionOccuredTimestamp: Date + var rateLimitSeconds: TimeInterval + + var rateLimitExpires: Date { + return actionOccuredTimestamp.addingTimeInterval(rateLimitSeconds) + } + } + + static var shared: RateLimitStorage = RateLimitStorage() // Singleton instance + + private var rateLimits = [String: RateLimiter]() + private var timer: Timer? + + func actionOccured(forKey key: String, rateLimit: TimeInterval) { + let now = Date() + if let existingRateLimit = rateLimits[key] { + if existingRateLimit.rateLimitExpires > now.addingTimeInterval(rateLimit) { + // We have an existing rate limit that is larger than the one being requested + // Ignore + return + } + } + self.objectWillChange.send() + rateLimits[key] = RateLimiter(actionOccuredTimestamp: now, rateLimitSeconds: rateLimit) + startTimerIfNecessary() + } + + func rateLimitRemainingPercentage(forKey: String) -> Double { + guard let rateLimit = rateLimits[forKey] else { + return 0.0 + } + let percent = (rateLimit.rateLimitExpires.timeIntervalSinceNow) / rateLimit.rateLimitSeconds + return min(1.0, max(percent, 0.0)) + } + + func rateLimitSecondsRemaining(forKey: String) -> TimeInterval { + guard let rateLimit = rateLimits[forKey] else { + return 0.0 + } + return rateLimit.rateLimitExpires.timeIntervalSinceNow + } + + func startTimerIfNecessary() { + // Timer exists, don't create one + guard timer == nil else { return } + + // Create the timer + self.timer = Timer.scheduledTimer(withTimeInterval: 1.0, repeats: true) { [weak self] _ in + guard let self = self else { return } + self.objectWillChange.send() + + // Determine if we can clean up the dictionary and stop the timer. + let maxExpiration = self.rateLimits.values.map { $0.rateLimitExpires }.max() ?? .distantPast + if maxExpiration.timeIntervalSinceNow < 0 { + // All rateLimits are in the past. Stop and clean up + self.timer?.invalidate() + self.timer = nil + self.rateLimits.removeAll() + } + } + } +} diff --git a/Meshtastic/Views/Nodes/Helpers/Actions/TraceRouteButton.swift b/Meshtastic/Views/Nodes/Helpers/Actions/TraceRouteButton.swift index 64e2563a..fe5d8c87 100644 --- a/Meshtastic/Views/Nodes/Helpers/Actions/TraceRouteButton.swift +++ b/Meshtastic/Views/Nodes/Helpers/Actions/TraceRouteButton.swift @@ -9,18 +9,28 @@ struct TraceRouteButton: View { private var isPresentingTraceRouteSentAlert: Bool = false var body: some View { - Button { + RateLimitedButton(key: "traceroute", rateLimit: 30.0) { isPresentingTraceRouteSentAlert = bleManager.sendTraceRouteRequest( destNum: node.user?.num ?? 0, wantResponse: true ) - } label: { - Label { - Text("Trace Route") - } icon: { - Image(systemName: "signpost.right.and.left") - .symbolRenderingMode(.hierarchical) - } + } label: { completion in + if let completion, completion.percentComplete > 0.0 { + Label { + Text("Trace Route (in \(completion.secondsRemaining.formatted(.number.precision(.fractionLength(0))))s)") + .foregroundStyle(.secondary) + } icon: { + Image("progress.ring.dashed", variableValue: completion.percentComplete) + .foregroundStyle(.secondary) + }.disabled(true) + } else { + Label { + Text("Trace Route") + } icon: { + Image(systemName: "signpost.right.and.left") + .symbolRenderingMode(.hierarchical) + } + } }.alert( "Trace Route Sent", isPresented: $isPresentingTraceRouteSentAlert diff --git a/Meshtastic/Views/Nodes/NodeList.swift b/Meshtastic/Views/Nodes/NodeList.swift index b4713dbd..44f968be 100644 --- a/Meshtastic/Views/Nodes/NodeList.swift +++ b/Meshtastic/Views/Nodes/NodeList.swift @@ -91,21 +91,10 @@ struct NodeList: View { Label("Message", systemImage: "message") } } - Button { - let traceRouteSent = bleManager.sendTraceRouteRequest( - destNum: node.num, - wantResponse: true - ) - if traceRouteSent { - isPresentingTraceRouteSentAlert = true - DispatchQueue.main.asyncAfter(deadline: .now() + 2.0) { - isPresentingTraceRouteSentAlert = false - } - } - - } label: { - Label("Trace Route", systemImage: "signpost.right.and.left") - } + TraceRouteButton( + bleManager: bleManager, + node: node + ) Button { let positionSent = bleManager.sendPosition( channel: node.channel, From b855ecc71332091bbc13f5868df58dedff7686f9 Mon Sep 17 00:00:00 2001 From: git bisector Date: Tue, 6 May 2025 19:36:42 -0700 Subject: [PATCH 012/213] Additional accessibilityLabels for VoiceOver users. --- Localizable.xcstrings | 480 +++++++++++++++++- Meshtastic/Extensions/String.swift | 11 + Meshtastic/Views/Bluetooth/Connect.swift | 30 ++ .../Helpers/BLESignalStrengthIndicator.swift | 82 +-- Meshtastic/Views/Helpers/BatteryCompact.swift | 115 +++-- Meshtastic/Views/Helpers/BatteryGauge.swift | 35 +- .../Views/Helpers/ConnectedDevice.swift | 50 +- .../RequestPositionButton.swift | 1 + .../TextMessageField/TextMessageSize.swift | 2 + .../Helpers/Actions/IgnoreNodeButton.swift | 1 + .../Views/Nodes/Helpers/NodeDetail.swift | 159 +++--- .../Views/Nodes/Helpers/NodeInfoItem.swift | 4 + .../Views/Nodes/Helpers/NodeListItem.swift | 95 +++- Meshtastic/Views/Nodes/NodeList.swift | 5 + 14 files changed, 906 insertions(+), 164 deletions(-) diff --git a/Localizable.xcstrings b/Localizable.xcstrings index aa5cbd4c..3a479de4 100644 --- a/Localizable.xcstrings +++ b/Localizable.xcstrings @@ -35342,7 +35342,485 @@ } } } + }, + "ble.signal.strength.weak" : { + "comment" : "VoiceOver value for weak BLE signal strength", + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "needs_review", + "value" : "Signalstärke schwach" + } + }, + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Signal strength weak" + } + }, + "it" : { + "stringUnit" : { + "state" : "needs_review", + "value" : "Segnale debole" + } + }, + "sr" : { + "stringUnit" : { + "state" : "needs_review", + "value" : "Слаб сигнал" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "needs_review", + "value" : "信号弱" + } + }, + "zh-Hant-TW" : { + "stringUnit" : { + "state" : "needs_review", + "value" : "訊號微弱" + } + } + } + }, + "signal_strength" : { + "comment" : "VoiceOver label for signal strength indicator", + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "needs_review", + "value" : "Signalstärke" + } + }, + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Signal strength" + } + }, + "it" : { + "stringUnit" : { + "state" : "needs_review", + "value" : "Intensità del segnale" + } + }, + "sr" : { + "stringUnit" : { + "state" : "needs_review", + "value" : "Јачина сигнала" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "needs_review", + "value" : "信号强度" + } + }, + "zh-Hant-TW" : { + "stringUnit" : { + "state" : "needs_review", + "value" : "訊號強度" + } + } + } + }, + "message_size" : { + "comment" : "VoiceOver label for message size", + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "needs_review", + "value" : "Nachrichtengröße" + } + }, + "en" : { + "stringUnit" : { + "state" : "needs_review", + "value" : "Message size" + } + }, + "it" : { + "stringUnit" : { + "state" : "needs_review", + "value" : "Dimensione messaggio" + } + }, + "sr" : { + "stringUnit" : { + "state" : "needs_review", + "value" : "Величина поруке" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "needs_review", + "value" : "消息大小" + } + }, + "zh-Hant-TW" : { + "stringUnit" : { + "state" : "needs_review", + "value" : "訊息大小" + } + } + } + }, + "device_charging" : { + "comment" : "VoiceOver value for charging device", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Charging" + } + } + } + }, + "Bluetooth is off.off" : { + "extractionState" : "stale", + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Bluetooth ist aus" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Le Bluetooth est arrêté" + } + }, + "he" : { + "stringUnit" : { + "state" : "translated", + "value" : "בלוטוס כבוי" + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Il Bluetooth è spento" + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Bluetooth jest wyłączony" + } + }, + "se" : { + "stringUnit" : { + "state" : "translated", + "value" : "Bluetooth är avstängt" + } + }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Блутут је искључен" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "蓝牙已关闭" + } + }, + "zh-Hant-TW" : { + "stringUnit" : { + "state" : "translated", + "value" : "藍芽已關閉" + } + } + } + }, + "bytes_used" : { + "comment" : "VoiceOver value for bytes used", + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "needs_review", + "value" : "%d von %d Bytes verwendet" + } + }, + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "%d of %d bytes used" + } + }, + "it" : { + "stringUnit" : { + "state" : "needs_review", + "value" : "%d di %d byte usati" + } + }, + "sr" : { + "stringUnit" : { + "state" : "needs_review", + "value" : "%d од %d бајтова искоришћено" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "needs_review", + "value" : "已用%d/%d字节" + } + }, + "zh-Hant-TW" : { + "stringUnit" : { + "state" : "needs_review", + "value" : "已用%d/%d位元組" + } + } + } + }, + "heading" : { + "comment" : "Heading label for VoiceOver" + }, + "Hide sidebar" : {}, + "bluetooth.not.connected" : { + "comment" : "VoiceOver label for disconnected Bluetooth icon", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "No Bluetooth device connected" + } + } + } + }, + "device.configuration" : { + "extractionState" : "stale", + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "needs_review", + "value" : "Gerätekonfiguration" + } + }, + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Device Configuration" + } + }, + "he" : { + "stringUnit" : { + "state" : "needs_review", + "value" : "Device Configuration" + } + }, + "it" : { + "stringUnit" : { + "state" : "needs_review", + "value" : "Configurazione del dispositivo" + } + }, + "pl" : { + "stringUnit" : { + "state" : "needs_review", + "value" : "Device Configuration" + } + }, + "se" : { + "stringUnit" : { + "state" : "needs_review", + "value" : "Enhetsinställningar" + } + }, + "sr" : { + "stringUnit" : { + "state" : "needs_review", + "value" : "Подешавања уређаја" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "needs_review", + "value" : "设备配置" + } + }, + "zh-Hant-TW" : { + "stringUnit" : { + "state" : "needs_review", + "value" : "設備設定" + } + } + } + }, + "ble.signal.strength.strong" : { + "comment" : "VoiceOver value for strong BLE signal strength", + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "needs_review", + "value" : "Signalstärke stark" + } + }, + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Signal strength strong" + } + }, + "it" : { + "stringUnit" : { + "state" : "needs_review", + "value" : "Segnale forte" + } + }, + "sr" : { + "stringUnit" : { + "state" : "needs_review", + "value" : "Јак сигнал" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "needs_review", + "value" : "信号强" + } + }, + "zh-Hant-TW" : { + "stringUnit" : { + "state" : "needs_review", + "value" : "訊號強" + } + } + } + }, + "ble.signal.strength.normal" : { + "comment" : "VoiceOver value for normal BLE signal strength", + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "needs_review", + "value" : "Signalstärke normal" + } + }, + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Signal strength normal" + } + }, + "it" : { + "stringUnit" : { + "state" : "needs_review", + "value" : "Segnale normale" + } + }, + "sr" : { + "stringUnit" : { + "state" : "needs_review", + "value" : "Нормалан сигнал" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "needs_review", + "value" : "信号正常" + } + }, + "zh-Hant-TW" : { + "stringUnit" : { + "state" : "needs_review", + "value" : "訊號正常" + } + } + } + }, + "bluetooth.connected" : { + "comment" : "VoiceOver label for connected Bluetooth icon", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Connected to Bluetooth device" + } + } + } + }, + "request_position" : { + "comment" : "VoiceOver label for request position button", + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Position anfordern" + } + }, + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Request position" + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Richiedi posizione" + } + }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Захтевај позицију" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "请求位置" + } + }, + "zh-Hant-TW" : { + "stringUnit" : { + "state" : "translated", + "value" : "請求位置" + } + } + } + }, + "distance" : { + "comment" : "Distance label for VoiceOver" + }, + "device_plugged_in" : { + "comment" : "VoiceOver value for plugged in device", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Plugged in" + } + } + } + }, + "unknown" : { + "localizations" : { + "it" : { + "stringUnit" : { + "state" : "needs_review", + "value" : "sconosciuto" + } + }, + "sr" : { + "stringUnit" : { + "state" : "needs_review", + "value" : "непознато" + } + }, + "zh-Hant-TW" : { + "stringUnit" : { + "state" : "needs_review", + "value" : "未知" + } + } + } } }, "version" : "1.0" -} \ No newline at end of file +} diff --git a/Meshtastic/Extensions/String.swift b/Meshtastic/Extensions/String.swift index d2ae1e5a..6a57da9e 100644 --- a/Meshtastic/Extensions/String.swift +++ b/Meshtastic/Extensions/String.swift @@ -115,6 +115,17 @@ extension String { .joined() } + /// Formats a short name like "P130" to read as "Node P 130" for VoiceOver + /// This ensures proper pronunciation of alphanumeric node IDs + func formatNodeNameForVoiceOver() -> String { + let spaced = self.replacingOccurrences( + of: #"([A-Za-z])([0-9]+)"#, + with: "$1 $2", + options: .regularExpression + ) + return "Node " + spaced + } + // Adds variation selectors to prefer the graphical form of emoji. // Looks ahead to make sure that the variation selector is not already applied. var addingVariationSelectors: String { diff --git a/Meshtastic/Views/Bluetooth/Connect.swift b/Meshtastic/Views/Bluetooth/Connect.swift index 5e9dd834..32d445bb 100644 --- a/Meshtastic/Views/Bluetooth/Connect.swift +++ b/Meshtastic/Views/Bluetooth/Connect.swift @@ -12,6 +12,7 @@ import CoreLocation import CoreBluetooth import OSLog import TipKit +import Foundation #if canImport(ActivityKit) import ActivityKit #endif @@ -27,6 +28,31 @@ struct Connect: View { @State var presentingSwitchPreferredPeripheral = false @State var selectedPeripherialId = "" + private func nodeAccessibilityLabel() -> String { + // Create a battery status string that handles charging and plugged in states + var batteryStatus: String? = nil + if let batteryLevel = node?.latestDeviceMetrics?.batteryLevel { + if batteryLevel > 100 { + // Plugged in state + batteryStatus = NSLocalizedString("device_plugged_in", comment: "VoiceOver value for plugged in device") + } else if batteryLevel == 100 { + // Charging state + batteryStatus = NSLocalizedString("device_charging", comment: "VoiceOver value for charging device") + } else { + // Normal battery percentage + batteryStatus = "Battery: \(Int(batteryLevel))%" + } + } + + return [ + node?.user?.shortName?.formatNodeNameForVoiceOver() ?? "", + "BLE Name: \(bleManager.connectedPeripheral?.peripheral.name?.addingVariationSelectors ?? "unknown".localized)", + "Firmware Version: \(node?.metadata?.firmwareVersion ?? "unknown".localized)", + bleManager.isSubscribed ? "Subscribed" : nil, + batteryStatus + ].compactMap { $0 }.joined(separator: ", ") + } + init () { let notificationCenter = UNUserNotificationCenter.current() notificationCenter.getNotificationSettings(completionHandler: { (settings) in @@ -86,6 +112,8 @@ struct Connect: View { } } } + .accessibilityElement(children: .ignore) + .accessibilityLabel(nodeAccessibilityLabel()) .font(.caption) .foregroundColor(Color.gray) .padding([.top]) @@ -299,6 +327,8 @@ struct Connect: View { mqttTopic: bleManager.mqttManager.topic ) } + // Make sure the ZStack passes through accessibility to the ConnectedDevice component + .accessibilityElement(children: .contain) ) } .sheet(isPresented: $invalidFirmwareVersion, onDismiss: didDismissSheet) { diff --git a/Meshtastic/Views/Helpers/BLESignalStrengthIndicator.swift b/Meshtastic/Views/Helpers/BLESignalStrengthIndicator.swift index c5d17f16..73d38f98 100644 --- a/Meshtastic/Views/Helpers/BLESignalStrengthIndicator.swift +++ b/Meshtastic/Views/Helpers/BLESignalStrengthIndicator.swift @@ -32,47 +32,65 @@ import Foundation import SwiftUI struct SignalStrengthIndicator: View { - let signalStrength: BLESignalStrength + // Accessibility: VoiceOver description + private var accessibilityDescription: String { + switch signalStrength { + case .weak: + return NSLocalizedString("ble.signal.strength.weak", comment: "VoiceOver value for weak BLE signal strength") + case .normal: + return NSLocalizedString("ble.signal.strength.normal", comment: "VoiceOver value for normal BLE signal strength") + case .strong: + return NSLocalizedString("ble.signal.strength.strong", comment: "VoiceOver value for strong BLE signal strength") + } + } - var body: some View { - HStack { - ForEach(0..<3) { bar in - RoundedRectangle(cornerRadius: 3) - .divided(amount: (CGFloat(bar) + 1) / CGFloat(3)) - .fill(getColor().opacity(bar <= signalStrength.rawValue ? 1 : 0.3)) - .frame(width: 8, height: 40) - } - } - } + let signalStrength: BLESignalStrength - private func getColor() -> Color { - switch signalStrength { - case .weak: - return Color.red - case .normal: - return Color.yellow - case .strong: - return Color.green - } - } + var body: some View { + Group { + HStack { + ForEach(0..<3) { bar in + RoundedRectangle(cornerRadius: 3) + .divided(amount: (CGFloat(bar) + 1) / CGFloat(3)) + .fill(getColor().opacity(bar <= signalStrength.rawValue ? 1 : 0.3)) + .frame(width: 8, height: 40) + accessibilityHidden(true) // Ensures bars are ignored + } + } + } + .accessibilityElement(children: .ignore) + .accessibilityLabel(NSLocalizedString("signal_strength", comment: "VoiceOver label for signal strength indicator")) + .accessibilityValue(accessibilityDescription) + } + + private func getColor() -> Color { + switch signalStrength { + case .weak: + return Color.red + case .normal: + return Color.yellow + case .strong: + return Color.green + } + } } struct Divided: Shape { - var amount: CGFloat // Should be in range 0...1 - var shape: S - func path(in rect: CGRect) -> Path { - shape.path(in: rect.divided(atDistance: amount * rect.height, from: .maxYEdge).slice) - } + var amount: CGFloat // Should be in range 0...1 + var shape: S + func path(in rect: CGRect) -> Path { + shape.path(in: rect.divided(atDistance: amount * rect.height, from: .maxYEdge).slice) + } } extension Shape { - func divided(amount: CGFloat) -> Divided { - return Divided(amount: amount, shape: self) - } + func divided(amount: CGFloat) -> Divided { + return Divided(amount: amount, shape: self) + } } enum BLESignalStrength: Int { - case weak = 0 - case normal = 1 - case strong = 2 + case weak = 0 + case normal = 1 + case strong = 2 } diff --git a/Meshtastic/Views/Helpers/BatteryCompact.swift b/Meshtastic/Views/Helpers/BatteryCompact.swift index 4ac61d0c..bb9819a2 100644 --- a/Meshtastic/Views/Helpers/BatteryCompact.swift +++ b/Meshtastic/Views/Helpers/BatteryCompact.swift @@ -13,69 +13,104 @@ struct BatteryCompact: View { var color: Color var body: some View { + // Group the battery icon and label in a single accessible container HStack(alignment: .center, spacing: 0) { if let batteryLevel { - if batteryLevel == 100 { - Image(systemName: "battery.100.bolt") - .font(iconFont) - .foregroundColor(color) - .symbolRenderingMode(.multicolor) - } else if batteryLevel < 100 && batteryLevel > 74 { - Image(systemName: "battery.75") - .font(iconFont) - .foregroundColor(color) - .symbolRenderingMode(.multicolor) - } else if batteryLevel < 75 && batteryLevel > 49 { - Image(systemName: "battery.50") - .font(iconFont) - .foregroundColor(color) - .symbolRenderingMode(.multicolor) - } else if batteryLevel < 50 && batteryLevel > 14 { - Image(systemName: "battery.25") - .font(iconFont) - .foregroundColor(color) - .symbolRenderingMode(.multicolor) - } else if batteryLevel < 15 && batteryLevel > 0 { - Image(systemName: "battery.0") - .font(iconFont) - .foregroundColor(color) - .symbolRenderingMode(.multicolor) - } else if batteryLevel == 0 { - Image(systemName: "battery.0") - .font(iconFont) - .foregroundColor(.red) - .symbolRenderingMode(.multicolor) - } else if batteryLevel > 100 { + // Check for plugged in state + let isPluggedIn = batteryLevel > 100 + let isCharging = batteryLevel == 100 + + // Battery icon selection based on level + if isPluggedIn { Image(systemName: "powerplug") .font(iconFont) .foregroundColor(color) .symbolRenderingMode(.multicolor) + .accessibilityHidden(true) // Hide from VoiceOver since container will handle it + } else if isCharging { + Image(systemName: "battery.100.bolt") + .font(iconFont) + .foregroundColor(color) + .symbolRenderingMode(.multicolor) + .accessibilityHidden(true) + } else if batteryLevel > 74 { + Image(systemName: "battery.75") + .font(iconFont) + .foregroundColor(color) + .symbolRenderingMode(.multicolor) + .accessibilityHidden(true) + } else if batteryLevel > 49 { + Image(systemName: "battery.50") + .font(iconFont) + .foregroundColor(color) + .symbolRenderingMode(.multicolor) + .accessibilityHidden(true) + } else if batteryLevel > 14 { + Image(systemName: "battery.25") + .font(iconFont) + .foregroundColor(color) + .symbolRenderingMode(.multicolor) + .accessibilityHidden(true) + } else if batteryLevel > 0 { + Image(systemName: "battery.0") + .font(iconFont) + .foregroundColor(color) + .symbolRenderingMode(.multicolor) + .accessibilityHidden(true) + } else { + Image(systemName: "battery.0") + .font(iconFont) + .foregroundColor(.red) + .symbolRenderingMode(.multicolor) + .accessibilityHidden(true) } - } else { - Image(systemName: "battery.0") - .font(iconFont) - .foregroundColor(color) - .symbolRenderingMode(.multicolor) - } - if let batteryLevel { - if batteryLevel > 100 { + + // Battery text label + if isPluggedIn { Text("PWD") .foregroundStyle(.secondary) .font(font) - } else if batteryLevel == 100 { + .accessibilityHidden(true) + } else if isCharging { Text("CHG") .foregroundStyle(.secondary) .font(font) + .accessibilityHidden(true) } else { Text(verbatim: "\(batteryLevel.formatted(.number.precision(.fractionLength(0))))%") .foregroundStyle(.secondary) .font(font) + .accessibilityHidden(true) } } else { + // Unknown battery state + Image(systemName: "battery.0") + .font(iconFont) + .foregroundColor(color) + .symbolRenderingMode(.multicolor) + .accessibilityHidden(true) + Text(verbatim: "?") .foregroundStyle(.secondary) .font(font) + .accessibilityHidden(true) } } + // Setup container-level accessibility for VoiceOver + .accessibilityElement(children: .ignore) + .accessibilityLabel(NSLocalizedString("Battery Level", comment: "VoiceOver label for battery gauge")) + // Set appropriate value based on the battery state using a computed property + .accessibilityValue(batteryLevel.map { level in + if level > 100 { + // Plugged in - same as PWD visual indicator + return NSLocalizedString("device_plugged_in", comment: "VoiceOver value for plugged in device") + } else if level == 100 { + // Charging - same as CHG visual indicator + return NSLocalizedString("device_charging", comment: "VoiceOver value for charging device") + } else { + // Normal battery level + return String(format: NSLocalizedString("battery_level_percent", comment: "VoiceOver value for battery level"), Int(level)) + } + } ?? "Unknown") } } diff --git a/Meshtastic/Views/Helpers/BatteryGauge.swift b/Meshtastic/Views/Helpers/BatteryGauge.swift index 952c9768..81e81e7e 100644 --- a/Meshtastic/Views/Helpers/BatteryGauge.swift +++ b/Meshtastic/Views/Helpers/BatteryGauge.swift @@ -18,18 +18,20 @@ struct BatteryGauge: View { let deviceMetrics = node.telemetries?.filtered(using: NSPredicate(format: "metricsType == 0")) let mostRecent = deviceMetrics?.lastObject as? TelemetryEntity - let batteryLevel = Double(mostRecent?.batteryLevel ?? 0) + // For VoiceOver purposes, detect when device is plugged in (battery > 100%) + let isPluggedIn = (mostRecent?.batteryLevel ?? 0) > 100 + // Use a capped battery level for UI display + let batteryLevel = Double(min(100, mostRecent?.batteryLevel ?? 0)) VStack { - if batteryLevel > 100.0 { - // Plugged in - Image(systemName: "powerplug") - .font(.largeTitle) - .foregroundColor(.accentColor) - .symbolRenderingMode(.hierarchical) + if isPluggedIn { + // Use a completely standalone view for the plugged in state + // to avoid any VoiceOver confusion + PluggedInIndicator() } else { let gradient = Gradient(colors: [.red, .orange, .green]) Gauge(value: batteryLevel, in: minValue...maxValue) { + // Accessibility for battery gauge if batteryLevel >= 0.0 && batteryLevel < 10 { Label("Battery Level %", systemImage: "battery.0") } else if batteryLevel >= 10.0 && batteryLevel < 25.00 { @@ -50,6 +52,8 @@ struct BatteryGauge: View { Text(Int(batteryLevel), format: .percent) } } + .accessibilityLabel(NSLocalizedString("Battery Level", comment: "VoiceOver label for battery gauge")) + .accessibilityValue(String(format: NSLocalizedString("Battery Level %", comment: "VoiceOver value for battery level"), Int(batteryLevel))) .tint(gradient) .gaugeStyle(.accessoryCircular) } @@ -63,6 +67,23 @@ struct BatteryGauge: View { } } +/// A dedicated view for showing a device is plugged in +/// With proper VoiceOver support that matches the visual indication +struct PluggedInIndicator: View { + var body: some View { + // This view is isolated from any battery measurement + // to ensure VoiceOver doesn't pick up any percentages + Image(systemName: "powerplug") + .font(.largeTitle) + .foregroundColor(.accentColor) + .symbolRenderingMode(.hierarchical) + // Override the accessibility to ensure correct VoiceOver announcement + .accessibilityElement(children: .ignore) + .accessibilityLabel(NSLocalizedString("Battery Level", comment: "VoiceOver label for battery gauge")) + .accessibilityValue(NSLocalizedString("device_plugged_in", comment: "VoiceOver value for plugged in device")) + } +} + struct BatteryGauge_Previews: PreviewProvider { static var previews: some View { VStack { diff --git a/Meshtastic/Views/Helpers/ConnectedDevice.swift b/Meshtastic/Views/Helpers/ConnectedDevice.swift index c795b1b0..4a46db41 100644 --- a/Meshtastic/Views/Helpers/ConnectedDevice.swift +++ b/Meshtastic/Views/Helpers/ConnectedDevice.swift @@ -21,22 +21,46 @@ struct ConnectedDevice: View { if (phoneOnly && UIDevice.current.userInterfaceIdiom == .phone) || !phoneOnly { if bluetoothOn { if deviceConnected { - if mqttUplinkEnabled || mqttDownlinkEnabled { - MQTTIcon(connected: mqttProxyConnected, uplink: mqttUplinkEnabled, downlink: mqttDownlinkEnabled, topic: mqttTopic) - } - Image(systemName: "antenna.radiowaves.left.and.right.circle.fill") - .imageScale(.large) - .foregroundColor(.green) - .symbolRenderingMode(.hierarchical) - Text(name.addingVariationSelectors).font(name.isEmoji() ? .title : .callout).foregroundColor(.gray) + // Create an HStack for connected state with proper accessibility + HStack { + if mqttUplinkEnabled || mqttDownlinkEnabled { + MQTTIcon(connected: mqttProxyConnected, uplink: mqttUplinkEnabled, downlink: mqttDownlinkEnabled, topic: mqttTopic) + .accessibilityHidden(true) + } + Image(systemName: "antenna.radiowaves.left.and.right.circle.fill") + .imageScale(.large) + .foregroundColor(.green) + .symbolRenderingMode(.hierarchical) + .accessibilityHidden(true) + Text(name.addingVariationSelectors) + .font(name.isEmoji() ? .title : .callout) + .foregroundColor(.gray) + .accessibilityHidden(true) + } + .accessibilityElement(children: .ignore) + .accessibilityLabel("bluetooth.connected".localized + ", " + name.formatNodeNameForVoiceOver()) } else { - Image(systemName: "antenna.radiowaves.left.and.right.slash") - .imageScale(.medium) - .foregroundColor(.red) - .symbolRenderingMode(.hierarchical) + // Create a container for disconnected state + HStack { + Image(systemName: "antenna.radiowaves.left.and.right.slash") + .imageScale(.medium) + .foregroundColor(.red) + .symbolRenderingMode(.hierarchical) + .accessibilityHidden(true) + } + .accessibilityElement(children: .ignore) + .accessibilityLabel("bluetooth.not.connected".localized) } } else { - Text("Bluetooth is off").font(.subheadline).foregroundColor(.red) + // Create a container for Bluetooth off state + HStack { + Text("bluetooth.off".localized) + .font(.subheadline) + .foregroundColor(.red) + .accessibilityHidden(true) + } + .accessibilityElement(children: .ignore) + .accessibilityLabel("bluetooth.off".localized) } } } diff --git a/Meshtastic/Views/Messages/TextMessageField/RequestPositionButton.swift b/Meshtastic/Views/Messages/TextMessageField/RequestPositionButton.swift index 2f1634bc..fd166f51 100644 --- a/Meshtastic/Views/Messages/TextMessageField/RequestPositionButton.swift +++ b/Meshtastic/Views/Messages/TextMessageField/RequestPositionButton.swift @@ -6,6 +6,7 @@ struct RequestPositionButton: View { var body: some View { Button(action: action) { Image(systemName: "mappin.and.ellipse") + .accessibilityLabel(NSLocalizedString("request_position", comment: "VoiceOver label for request position button")) .symbolRenderingMode(.hierarchical) .imageScale(.large) .foregroundColor(.accentColor) diff --git a/Meshtastic/Views/Messages/TextMessageField/TextMessageSize.swift b/Meshtastic/Views/Messages/TextMessageField/TextMessageSize.swift index aacbd60d..9839e246 100644 --- a/Meshtastic/Views/Messages/TextMessageField/TextMessageSize.swift +++ b/Meshtastic/Views/Messages/TextMessageField/TextMessageSize.swift @@ -6,6 +6,8 @@ struct TextMessageSize: View { var body: some View { ProgressView("\("Bytes".localized): \(totalBytes) / \(maxbytes)", value: Double(totalBytes), total: Double(maxbytes)) + .accessibilityLabel(NSLocalizedString("message_size", comment: "VoiceOver label for message size")) + .accessibilityValue(String(format: NSLocalizedString("bytes_used", comment: "VoiceOver value for bytes used"), totalBytes, maxbytes)) .frame(width: 130) .padding(5) .font(.subheadline) diff --git a/Meshtastic/Views/Nodes/Helpers/Actions/IgnoreNodeButton.swift b/Meshtastic/Views/Nodes/Helpers/Actions/IgnoreNodeButton.swift index 84fdf4d3..2d73d5c0 100644 --- a/Meshtastic/Views/Nodes/Helpers/Actions/IgnoreNodeButton.swift +++ b/Meshtastic/Views/Nodes/Helpers/Actions/IgnoreNodeButton.swift @@ -40,6 +40,7 @@ struct IgnoreNodeButton: View { Image(systemName: node.ignored ? "minus.circle.fill" : "minus.circle") .symbolRenderingMode(.multicolor) } + // Accessibility: Label for VoiceOver } } } diff --git a/Meshtastic/Views/Nodes/Helpers/NodeDetail.swift b/Meshtastic/Views/Nodes/Helpers/NodeDetail.swift index 081e7adc..c5670e06 100644 --- a/Meshtastic/Views/Nodes/Helpers/NodeDetail.swift +++ b/Meshtastic/Views/Nodes/Helpers/NodeDetail.swift @@ -46,7 +46,8 @@ struct NodeDetail: View { Section("Hardware") { NodeInfoItem(node: node) } - Section("Node") { + .accessibilityElement(children: .combine) + Section("Node") { // Node HStack(alignment: .center) { Spacer() CircleText( @@ -67,6 +68,7 @@ struct NodeDetail: View { .foregroundColor(getRssiColor(rssi: node.rssi)) .font(.caption) } + .accessibilityElement(children: .combine) } if node.telemetries?.count ?? 0 > 0 { Spacer() @@ -74,6 +76,7 @@ struct NodeDetail: View { } Spacer() } + .accessibilityElement(children: .combine) .listRowSeparator(.hidden) if let user = node.user { if !user.keyMatch { @@ -86,6 +89,7 @@ struct NodeDetail: View { .foregroundStyle(.secondary) .font(.callout) } + .accessibilityElement(children: .combine) } icon: { Image(systemName: "key.slash.fill") .symbolRenderingMode(.multicolor) @@ -104,6 +108,7 @@ struct NodeDetail: View { Text(String(node.num)) .textSelection(.enabled) } + .accessibilityElement(children: .combine) HStack { Label { @@ -116,6 +121,7 @@ struct NodeDetail: View { Text(node.num.toHex()) .textSelection(.enabled) } + .accessibilityElement(children: .combine) if let metadata = node.metadata { HStack { @@ -129,6 +135,7 @@ struct NodeDetail: View { Text(metadata.firmwareVersion ?? "Unknown".localized) } + .accessibilityElement(children: .combine) } if let role = node.user?.role, let deviceRole = DeviceRoles(rawValue: Int(role)) { @@ -142,6 +149,7 @@ struct NodeDetail: View { Spacer() Text(deviceRole.name) } + .accessibilityElement(children: .combine) } if let dm = node.telemetries?.filtered(using: NSPredicate(format: "metricsType == 0")).lastObject as? TelemetryEntity, let uptimeSeconds = dm.uptimeSeconds { @@ -161,6 +169,7 @@ struct NodeDetail: View { Text(uptime) .textSelection(.enabled) } + .accessibilityElement(children: .combine) } if let firstHeard = node.firstHeard, firstHeard.timeIntervalSince1970 > 0 && firstHeard < Calendar.current.date(byAdding: .year, value: 1, to: Date())! { @@ -179,7 +188,9 @@ struct NodeDetail: View { Text(firstHeard.formatted()) .textSelection(.enabled) } - }.onTapGesture { + } + .accessibilityElement(children: .combine) + .onTapGesture { dateFormatRelative.toggle() } } @@ -203,7 +214,9 @@ struct NodeDetail: View { Text(lastHeard.formatted()) .textSelection(.enabled) } - }.onTapGesture { + } + .accessibilityElement(children: .combine) + .onTapGesture { dateFormatRelative.toggle() } } @@ -216,79 +229,84 @@ struct NodeDetail: View { if node.hasPositions && UserDefaults.environmentEnableWeatherKit || node.hasDataForLatestEnvironmentMetrics(attributes: ["iaq", "temperature", "relativeHumidity", "barometricPressure", "windSpeed", "radiation", "weight", "Distance", "soilTemperature", "soilMoisture"]) { Section("Environment") { - if !node.hasEnvironmentMetrics { - LocalWeatherConditions(location: node.latestPosition?.nodeLocation) - } else { - VStack { - if node.latestEnvironmentMetrics?.iaq ?? -1 > 0 { - IndoorAirQuality(iaq: Int(node.latestEnvironmentMetrics?.iaq ?? 0), displayMode: .gradient) - .padding(.vertical) - } - LazyVGrid(columns: gridItemLayout) { - if let temperature = node.latestEnvironmentMetrics?.temperature?.shortFormattedTemperature() { - WeatherConditionsCompactWidget(temperature: String(temperature), symbolName: "cloud.sun", description: "TEMP") + // Group weather/environment data for better VoiceOver experience + VStack { + if !node.hasEnvironmentMetrics { + LocalWeatherConditions(location: node.latestPosition?.nodeLocation) + } else { + VStack { + if node.latestEnvironmentMetrics?.iaq ?? -1 > 0 { + IndoorAirQuality(iaq: Int(node.latestEnvironmentMetrics?.iaq ?? 0), displayMode: .gradient) + .padding(.vertical) } - if let humidity = node.latestEnvironmentMetrics?.relativeHumidity { - if let temperature = node.latestEnvironmentMetrics?.temperature { - let dewPoint = calculateDewPoint(temp: temperature, relativeHumidity: humidity) - .formatted(.number.precision(.fractionLength(0))) + "°" - HumidityCompactWidget(humidity: Int(humidity), dewPoint: dewPoint) - } else { - HumidityCompactWidget(humidity: Int(humidity), dewPoint: nil) + LazyVGrid(columns: gridItemLayout) { + if let temperature = node.latestEnvironmentMetrics?.temperature?.shortFormattedTemperature() { + WeatherConditionsCompactWidget(temperature: String(temperature), symbolName: "cloud.sun", description: "TEMP") + } + if let humidity = node.latestEnvironmentMetrics?.relativeHumidity { + if let temperature = node.latestEnvironmentMetrics?.temperature { + let dewPoint = calculateDewPoint(temp: temperature, relativeHumidity: humidity) + .formatted(.number.precision(.fractionLength(0))) + "°" + HumidityCompactWidget(humidity: Int(humidity), dewPoint: dewPoint) + } else { + HumidityCompactWidget(humidity: Int(humidity), dewPoint: nil) + } + } + if let pressure = node.latestEnvironmentMetrics?.barometricPressure { + PressureCompactWidget(pressure: pressure.formatted(.number.precision(.fractionLength(2))), unit: "hPA", low: pressure <= 1009.144) + } + if let windSpeed = node.latestEnvironmentMetrics?.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)) + 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) + } + if let rainfall1h = node.latestEnvironmentMetrics?.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 + let unitLabel = usesMetricSystem ? "mm" : "in" + let measurement = Measurement(value: Double(rainfall1h), unit: UnitLength.millimeters) + let decimals = usesMetricSystem ? 0 : 1 + 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 { + 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 + let unitLabel = usesMetricSystem ? "mm" : "in" + let measurement = Measurement(value: Double(rainfall24h), unit: UnitLength.millimeters) + let decimals = usesMetricSystem ? 0 : 1 + 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 { + RadiationCompactWidget(radiation: radiation.formatted(.number.precision(.fractionLength(1))), unit: "µR/hr") + } + if let weight = node.latestEnvironmentMetrics?.weight { + WeightCompactWidget(weight: weight.formatted(.number.precision(.fractionLength(1))), unit: "kg") + } + if let distance = node.latestEnvironmentMetrics?.distance { + DistanceCompactWidget(distance: distance.formatted(.number.precision(.fractionLength(0))), unit: "mm") + } + if let soilTemperature = node.latestEnvironmentMetrics?.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 { + SoilMoistureCompactWidget(moisture: soilMoisture.formatted(.number.precision(.fractionLength(0))), unit: "%") } } - if let pressure = node.latestEnvironmentMetrics?.barometricPressure { - PressureCompactWidget(pressure: pressure.formatted(.number.precision(.fractionLength(2))), unit: "hPA", low: pressure <= 1009.144) - } - if let windSpeed = node.latestEnvironmentMetrics?.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)) - 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) - } - if let rainfall1h = node.latestEnvironmentMetrics?.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 - let unitLabel = usesMetricSystem ? "mm" : "in" - let measurement = Measurement(value: Double(rainfall1h), unit: UnitLength.millimeters) - let decimals = usesMetricSystem ? 0 : 1 - 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 { - 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 - let unitLabel = usesMetricSystem ? "mm" : "in" - let measurement = Measurement(value: Double(rainfall24h), unit: UnitLength.millimeters) - let decimals = usesMetricSystem ? 0 : 1 - 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 { - RadiationCompactWidget(radiation: radiation.formatted(.number.precision(.fractionLength(1))), unit: "µR/hr") - } - if let weight = node.latestEnvironmentMetrics?.weight { - WeightCompactWidget(weight: weight.formatted(.number.precision(.fractionLength(1))), unit: "kg") - } - if let distance = node.latestEnvironmentMetrics?.distance { - DistanceCompactWidget(distance: distance.formatted(.number.precision(.fractionLength(0))), unit: "mm") - } - if let soilTemperature = node.latestEnvironmentMetrics?.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 { - SoilMoistureCompactWidget(moisture: soilMoisture.formatted(.number.precision(.fractionLength(0))), unit: "%") - } + .padding(node.latestEnvironmentMetrics?.iaq ?? -1 > 0 ? .bottom : .vertical) } - .padding(node.latestEnvironmentMetrics?.iaq ?? -1 > 0 ? .bottom : .vertical) } } + // Apply accessibility properties to the environment section + .accessibilityElement(children: .combine) } } if node.hasPowerMetrics && node.latestPowerMetrics != nil { @@ -298,6 +316,7 @@ struct NodeDetail: View { PowerMetrics(metric: metric) } } + .accessibilityElement(children: .combine) } } Section("Logs") { diff --git a/Meshtastic/Views/Nodes/Helpers/NodeInfoItem.swift b/Meshtastic/Views/Nodes/Helpers/NodeInfoItem.swift index 07f3d92c..eb4c37b0 100644 --- a/Meshtastic/Views/Nodes/Helpers/NodeInfoItem.swift +++ b/Meshtastic/Views/Nodes/Helpers/NodeInfoItem.swift @@ -31,6 +31,7 @@ struct NodeInfoItem: View { .foregroundStyle(.gray) .font(.callout) } + .accessibilityElement(children: .combine) Spacer() } VStack(alignment: .center) { @@ -49,9 +50,11 @@ struct NodeInfoItem: View { .cornerRadius(5) } } + .accessibilityElement(children: .combine) } Spacer() } + .accessibilityElement(children: .combine) .onAppear { Api().loadDeviceHardwareData { (hw) in for device in hw { @@ -79,6 +82,7 @@ struct NodeInfoItem: View { Text(String("incomplete".localized)) } } + .accessibilityElement(children: .combine) } } } diff --git a/Meshtastic/Views/Nodes/Helpers/NodeListItem.swift b/Meshtastic/Views/Nodes/Helpers/NodeListItem.swift index 2978ceab..62dd5fd0 100644 --- a/Meshtastic/Views/Nodes/Helpers/NodeListItem.swift +++ b/Meshtastic/Views/Nodes/Helpers/NodeListItem.swift @@ -7,9 +7,99 @@ import SwiftUI import CoreLocation +import Foundation struct NodeListItem: View { + // Accessibility: Synthesized description for VoiceOver + private var accessibilityDescription: String { + var desc = "" + if let shortName = node.user?.shortName { + // Format the shortName using the String extension method + desc = shortName.formatNodeNameForVoiceOver() + } else if let longName = node.user?.longName { + desc = longName + } else { + desc = "unknown node" + } + if connected { + desc += ", currently connected" + } + if node.favorite { + desc += ", favorite" + } + if node.lastHeard != nil { + let formatter = RelativeDateTimeFormatter() + formatter.unitsStyle = .full + let relative = formatter.localizedString(for: node.lastHeard!, relativeTo: Date()) + desc += ", last heard " + relative + } + if node.isOnline { + desc += ", online" + } else { + desc += ", offline" + } + let role = DeviceRoles(rawValue: Int(node.user?.role ?? 0)) + if let roleName = role?.name { + desc += ", role: \(roleName)" + } + if node.hopsAway > 0 { + desc += ", \(node.hopsAway) hops away" + } + if let battery = node.latestDeviceMetrics?.batteryLevel { + // Check for plugged in and charging states, same logic as in BatteryCompact and BatteryGauge + if battery > 100 { + desc += ", " + NSLocalizedString("device_plugged_in", comment: "VoiceOver value for plugged in device") + } else if battery == 100 { + desc += ", " + NSLocalizedString("device_charging", comment: "VoiceOver value for charging device") + } else { + desc += ", battery \(battery)%" + } + } + // Add distance and heading/bearing if available, but only for non-connected nodes + if !connected, let (lastPosition, myCoord) = locationData { + let nodeCoord = CLLocation(latitude: lastPosition.nodeCoordinate!.latitude, longitude: lastPosition.nodeCoordinate!.longitude) + let metersAway = nodeCoord.distance(from: myCoord) + + // Distance information + let distanceFormatter = LengthFormatter() + distanceFormatter.unitStyle = .medium + let formattedDistance = distanceFormatter.string(fromMeters: metersAway) + // For VoiceOver, prepend 'Distance' (localized) + desc += ", " + String(format: "%@: %@", NSLocalizedString("distance", comment: "Distance label for VoiceOver"), formattedDistance) + + // Add bearing/heading information for VoiceOver + let trueBearing = getBearingBetweenTwoPoints(point1: myCoord, point2: nodeCoord) + let heading = Measurement(value: trueBearing, unit: UnitAngle.degrees) + let formattedHeading = heading.formatted(.measurement(width: .narrow, numberFormatStyle: .number.precision(.fractionLength(0)))) + // Using a direct format without requiring a new localization key + desc += ", " + NSLocalizedString("heading", comment: "Heading label for VoiceOver") + " " + formattedHeading + } + // Add signal strength if available + if node.snr != 0 && !node.viaMqtt { + let signalStrength: BLESignalStrength + if node.snr < -10 { + signalStrength = .weak + } else if node.snr < 5 { + signalStrength = .normal + } else { + signalStrength = .strong + } + let signalString: String + switch signalStrength { + case .weak: + signalString = NSLocalizedString("ble.signal.strength.weak", comment: "VoiceOver value for weak BLE signal strength") + case .normal: + signalString = NSLocalizedString("ble.signal.strength.normal", comment: "VoiceOver value for normal BLE signal strength") + case .strong: + signalString = NSLocalizedString("ble.signal.strength.strong", comment: "VoiceOver value for strong BLE signal strength") + } + desc += ", " + signalString + } + return desc + } + + @ObservedObject var node: NodeInfoEntity var connected: Bool var connectedNode: Int64 @@ -167,7 +257,10 @@ struct NodeListItem: View { } .padding(.top, 4) .padding(.bottom, 4) - } + // Accessibility: Make the whole row a single element for VoiceOver + .accessibilityElement(children: .ignore) + .accessibilityLabel(accessibilityDescription) + } } struct DefaultIcon: View { diff --git a/Meshtastic/Views/Nodes/NodeList.swift b/Meshtastic/Views/Nodes/NodeList.swift index 1e823020..6b305148 100644 --- a/Meshtastic/Views/Nodes/NodeList.swift +++ b/Meshtastic/Views/Nodes/NodeList.swift @@ -243,6 +243,8 @@ struct NodeList: View { phoneOnly: true ) } + // Make sure the ZStack passes through accessibility to the ConnectedDevice component + .accessibilityElement(children: .contain) ) } content: { if let node = selectedNode { @@ -261,6 +263,7 @@ struct NodeList: View { } label: { Image(systemName: "rectangle") } + .accessibilityLabel("Hide sidebar") } ConnectedDevice( bluetoothOn: bleManager.isSwitchedOn, @@ -269,6 +272,8 @@ struct NodeList: View { phoneOnly: true ) } + // Make sure the ZStack passes through accessibility to the ConnectedDevice component + .accessibilityElement(children: .contain) ) } } else { From 7063e7d419d556d1bd9b374688f6719bc7883253 Mon Sep 17 00:00:00 2001 From: git bisector Date: Fri, 9 May 2025 19:20:27 -0700 Subject: [PATCH 013/213] Fix TraceRoute notification navigation to correct node (#1115) When clicking on a completed TraceRoute notification, the app now navigates to the correct destination node instead of the connected node. This fixes issue #1115 where the app was navigating to the wrong node detail screen. --- Meshtastic/Helpers/BLEManager.swift | 2 +- Meshtastic/MeshtasticAppDelegate.swift | 17 ++++++++++++++- Meshtastic/Views/Nodes/NodeList.swift | 30 +++++++++++++++++++++++++- 3 files changed, 46 insertions(+), 3 deletions(-) diff --git a/Meshtastic/Helpers/BLEManager.swift b/Meshtastic/Helpers/BLEManager.swift index ba65f66a..b5971896 100644 --- a/Meshtastic/Helpers/BLEManager.swift +++ b/Meshtastic/Helpers/BLEManager.swift @@ -940,7 +940,7 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate subtitle: "TR received back from \(destinationHop.name ?? "unknown")", content: "Hops from: \(tr.hopsTowards), Hops back: \(tr.hopsBack)\n\(tr.routeText ?? "Unknown".localized)\n\(tr.routeBackText ?? "Unknown".localized)", target: "nodes", - path: "meshtastic:///nodes?nodenum=\(connectedNode.user?.num ?? 0)" + path: "meshtastic:///nodes?nodenum=\(tr.node?.num ?? 0)" ) ] manager.schedule() diff --git a/Meshtastic/MeshtasticAppDelegate.swift b/Meshtastic/MeshtasticAppDelegate.swift index 365faef4..f4c405bb 100644 --- a/Meshtastic/MeshtasticAppDelegate.swift +++ b/Meshtastic/MeshtasticAppDelegate.swift @@ -97,7 +97,22 @@ class MeshtasticAppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificat if let targetValue = userInfo["target"] as? String, let deepLink = userInfo["path"] as? String, let url = URL(string: deepLink) { - Logger.services.info("userNotificationCenter didReceiveResponse \(targetValue, privacy: .public) \(deepLink, privacy: .public)") + Logger.services.info("userNotificationCenter didReceiveResponse handling deeplink: \(targetValue, privacy: .public) \(deepLink, privacy: .public)") + // Handle TraceRoute notifications specially to ensure they navigate correctly + if deepLink.contains("meshtastic:///nodes") && deepLink.contains("nodenum=") { + // First extract the node number from the URL + if let nodeNumString = deepLink.components(separatedBy: "nodenum=").last, + let nodeNum = Int64(nodeNumString) { + Logger.services.info("Navigation to specific node via notification: \(nodeNum, privacy: .public)") + self.router?.navigationState.selectedTab = .nodes + // Post a notification to trigger app-wide refresh + NotificationCenter.default.post(name: NSNotification.Name("ForceNavigationRefresh"), + object: nil, + userInfo: ["nodeNum": nodeNum]) + self.router?.navigationState.nodeListSelectedNodeNum = nodeNum + } + } + // Still call the regular router in all cases router?.route(url: url) } else { Logger.services.error("Failed to handle notification response: \(userInfo, privacy: .public)") diff --git a/Meshtastic/Views/Nodes/NodeList.swift b/Meshtastic/Views/Nodes/NodeList.swift index 1e823020..5a531ec2 100644 --- a/Meshtastic/Views/Nodes/NodeList.swift +++ b/Meshtastic/Views/Nodes/NodeList.swift @@ -28,6 +28,8 @@ struct NodeList: View { @State private var isFavorite = false @State private var isIgnored = false @State private var isEnvironment = false + // Force refresh ID to make SwiftUI rebuild the view hierarchy + @State private var forceRefreshID = UUID() @State private var distanceFilter = false @State private var maxDistance: Double = 800000 @State private var hopsAway: Double = -1.0 @@ -142,6 +144,7 @@ struct NodeList: View { } var body: some View { + // Use forceRefreshID to completely rebuild the view when notifications update the selected node NavigationSplitView(columnVisibility: $columnVisibility) { List(nodes, id: \.self, selection: $selectedNode) { node in NodeListItem( @@ -326,16 +329,41 @@ struct NodeList: View { } .onChange(of: router.navigationState) { if let selected = router.navigationState.nodeListSelectedNodeNum { - self.selectedNode = getNodeInfo(id: selected, context: context) + // Force a complete view rebuild by generating a new UUID + Logger.services.info("Forcing view rebuild with new ID: \(self.forceRefreshID)") + // First clear selection + self.forceRefreshID = UUID() + self.selectedNode = nil + // Then after a short delay, set the new selection. Makes it obvious to use page is refreshing too. + DispatchQueue.main.asyncAfter(deadline: .now() + 0.15) { + // Generate another UUID to ensure view gets rebuilt + self.forceRefreshID = UUID() + self.selectedNode = getNodeInfo(id: selected, context: context) + Logger.services.info("Complete view refresh with node: \(selected, privacy: .public)") + } } else { self.selectedNode = nil } } .onAppear { + // Set up notification observer for forced refreshes from notifications + NotificationCenter.default.addObserver(forName: NSNotification.Name("ForceNavigationRefresh"), object: nil, queue: .main) { notification in + if let nodeNum = notification.userInfo?["nodeNum"] as? Int64 { + // Force complete refresh of view + self.forceRefreshID = UUID() + self.selectedNode = getNodeInfo(id: nodeNum, context: self.context) + Logger.services.info("NodeList directly updated from notification for node: \(nodeNum, privacy: .public)") + } + } + Task { await searchNodeList() } } + .onDisappear { + // Remove observer when view disappears + NotificationCenter.default.removeObserver(self, name: NSNotification.Name("ForceNavigationRefresh"), object: nil) + } } private func searchNodeList() async { From 37828fb62f0692730e312e0a19e4511031b2a4b9 Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Sat, 10 May 2025 08:46:28 -0700 Subject: [PATCH 014/213] 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 403492d2..3021f949 100644 --- a/Meshtastic.xcodeproj/project.pbxproj +++ b/Meshtastic.xcodeproj/project.pbxproj @@ -1785,7 +1785,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 2.6.2; + MARKETING_VERSION = 2.6.3; PRODUCT_BUNDLE_IDENTIFIER = gvh.MeshtasticClient; PRODUCT_NAME = "$(TARGET_NAME)"; SUPPORTS_MACCATALYST = YES; @@ -1818,7 +1818,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 2.6.2; + MARKETING_VERSION = 2.6.3; PRODUCT_BUNDLE_IDENTIFIER = gvh.MeshtasticClient; PRODUCT_NAME = "$(TARGET_NAME)"; SUPPORTS_MACCATALYST = YES; @@ -1849,7 +1849,7 @@ "@executable_path/Frameworks", "@executable_path/../../Frameworks", ); - MARKETING_VERSION = 2.6.2; + MARKETING_VERSION = 2.6.3; PRODUCT_BUNDLE_IDENTIFIER = gvh.MeshtasticClient.Widgets; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; @@ -1881,7 +1881,7 @@ "@executable_path/Frameworks", "@executable_path/../../Frameworks", ); - MARKETING_VERSION = 2.6.2; + MARKETING_VERSION = 2.6.3; PRODUCT_BUNDLE_IDENTIFIER = gvh.MeshtasticClient.Widgets; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; From 07358b18e24bceff8cb789b3a72d02c057ee0748 Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Sat, 10 May 2025 18:32:31 -0700 Subject: [PATCH 015/213] Revert "Additional accessibilityLabels for VoiceOver users." --- Localizable.xcstrings | 480 +----------------- Meshtastic/Extensions/String.swift | 11 - Meshtastic/Views/Bluetooth/Connect.swift | 30 -- .../Helpers/BLESignalStrengthIndicator.swift | 82 ++- Meshtastic/Views/Helpers/BatteryCompact.swift | 89 +--- Meshtastic/Views/Helpers/BatteryGauge.swift | 35 +- .../Views/Helpers/ConnectedDevice.swift | 50 +- .../RequestPositionButton.swift | 1 - .../TextMessageField/TextMessageSize.swift | 2 - .../Helpers/Actions/IgnoreNodeButton.swift | 1 - .../Views/Nodes/Helpers/NodeDetail.swift | 165 +++--- .../Views/Nodes/Helpers/NodeInfoItem.swift | 4 - .../Views/Nodes/Helpers/NodeListItem.swift | 95 +--- Meshtastic/Views/Nodes/NodeList.swift | 5 - 14 files changed, 154 insertions(+), 896 deletions(-) diff --git a/Localizable.xcstrings b/Localizable.xcstrings index 0fabb557..c4c9d2ca 100644 --- a/Localizable.xcstrings +++ b/Localizable.xcstrings @@ -35345,485 +35345,7 @@ } } } - }, - "ble.signal.strength.weak" : { - "comment" : "VoiceOver value for weak BLE signal strength", - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "needs_review", - "value" : "Signalstärke schwach" - } - }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Signal strength weak" - } - }, - "it" : { - "stringUnit" : { - "state" : "needs_review", - "value" : "Segnale debole" - } - }, - "sr" : { - "stringUnit" : { - "state" : "needs_review", - "value" : "Слаб сигнал" - } - }, - "zh-Hans" : { - "stringUnit" : { - "state" : "needs_review", - "value" : "信号弱" - } - }, - "zh-Hant-TW" : { - "stringUnit" : { - "state" : "needs_review", - "value" : "訊號微弱" - } - } - } - }, - "signal_strength" : { - "comment" : "VoiceOver label for signal strength indicator", - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "needs_review", - "value" : "Signalstärke" - } - }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Signal strength" - } - }, - "it" : { - "stringUnit" : { - "state" : "needs_review", - "value" : "Intensità del segnale" - } - }, - "sr" : { - "stringUnit" : { - "state" : "needs_review", - "value" : "Јачина сигнала" - } - }, - "zh-Hans" : { - "stringUnit" : { - "state" : "needs_review", - "value" : "信号强度" - } - }, - "zh-Hant-TW" : { - "stringUnit" : { - "state" : "needs_review", - "value" : "訊號強度" - } - } - } - }, - "message_size" : { - "comment" : "VoiceOver label for message size", - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "needs_review", - "value" : "Nachrichtengröße" - } - }, - "en" : { - "stringUnit" : { - "state" : "needs_review", - "value" : "Message size" - } - }, - "it" : { - "stringUnit" : { - "state" : "needs_review", - "value" : "Dimensione messaggio" - } - }, - "sr" : { - "stringUnit" : { - "state" : "needs_review", - "value" : "Величина поруке" - } - }, - "zh-Hans" : { - "stringUnit" : { - "state" : "needs_review", - "value" : "消息大小" - } - }, - "zh-Hant-TW" : { - "stringUnit" : { - "state" : "needs_review", - "value" : "訊息大小" - } - } - } - }, - "device_charging" : { - "comment" : "VoiceOver value for charging device", - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Charging" - } - } - } - }, - "Bluetooth is off.off" : { - "extractionState" : "stale", - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Bluetooth ist aus" - } - }, - "fr" : { - "stringUnit" : { - "state" : "translated", - "value" : "Le Bluetooth est arrêté" - } - }, - "he" : { - "stringUnit" : { - "state" : "translated", - "value" : "בלוטוס כבוי" - } - }, - "it" : { - "stringUnit" : { - "state" : "translated", - "value" : "Il Bluetooth è spento" - } - }, - "pl" : { - "stringUnit" : { - "state" : "translated", - "value" : "Bluetooth jest wyłączony" - } - }, - "se" : { - "stringUnit" : { - "state" : "translated", - "value" : "Bluetooth är avstängt" - } - }, - "sr" : { - "stringUnit" : { - "state" : "translated", - "value" : "Блутут је искључен" - } - }, - "zh-Hans" : { - "stringUnit" : { - "state" : "translated", - "value" : "蓝牙已关闭" - } - }, - "zh-Hant-TW" : { - "stringUnit" : { - "state" : "translated", - "value" : "藍芽已關閉" - } - } - } - }, - "bytes_used" : { - "comment" : "VoiceOver value for bytes used", - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "needs_review", - "value" : "%d von %d Bytes verwendet" - } - }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "%d of %d bytes used" - } - }, - "it" : { - "stringUnit" : { - "state" : "needs_review", - "value" : "%d di %d byte usati" - } - }, - "sr" : { - "stringUnit" : { - "state" : "needs_review", - "value" : "%d од %d бајтова искоришћено" - } - }, - "zh-Hans" : { - "stringUnit" : { - "state" : "needs_review", - "value" : "已用%d/%d字节" - } - }, - "zh-Hant-TW" : { - "stringUnit" : { - "state" : "needs_review", - "value" : "已用%d/%d位元組" - } - } - } - }, - "heading" : { - "comment" : "Heading label for VoiceOver" - }, - "Hide sidebar" : {}, - "bluetooth.not.connected" : { - "comment" : "VoiceOver label for disconnected Bluetooth icon", - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "No Bluetooth device connected" - } - } - } - }, - "device.configuration" : { - "extractionState" : "stale", - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "needs_review", - "value" : "Gerätekonfiguration" - } - }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Device Configuration" - } - }, - "he" : { - "stringUnit" : { - "state" : "needs_review", - "value" : "Device Configuration" - } - }, - "it" : { - "stringUnit" : { - "state" : "needs_review", - "value" : "Configurazione del dispositivo" - } - }, - "pl" : { - "stringUnit" : { - "state" : "needs_review", - "value" : "Device Configuration" - } - }, - "se" : { - "stringUnit" : { - "state" : "needs_review", - "value" : "Enhetsinställningar" - } - }, - "sr" : { - "stringUnit" : { - "state" : "needs_review", - "value" : "Подешавања уређаја" - } - }, - "zh-Hans" : { - "stringUnit" : { - "state" : "needs_review", - "value" : "设备配置" - } - }, - "zh-Hant-TW" : { - "stringUnit" : { - "state" : "needs_review", - "value" : "設備設定" - } - } - } - }, - "ble.signal.strength.strong" : { - "comment" : "VoiceOver value for strong BLE signal strength", - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "needs_review", - "value" : "Signalstärke stark" - } - }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Signal strength strong" - } - }, - "it" : { - "stringUnit" : { - "state" : "needs_review", - "value" : "Segnale forte" - } - }, - "sr" : { - "stringUnit" : { - "state" : "needs_review", - "value" : "Јак сигнал" - } - }, - "zh-Hans" : { - "stringUnit" : { - "state" : "needs_review", - "value" : "信号强" - } - }, - "zh-Hant-TW" : { - "stringUnit" : { - "state" : "needs_review", - "value" : "訊號強" - } - } - } - }, - "ble.signal.strength.normal" : { - "comment" : "VoiceOver value for normal BLE signal strength", - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "needs_review", - "value" : "Signalstärke normal" - } - }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Signal strength normal" - } - }, - "it" : { - "stringUnit" : { - "state" : "needs_review", - "value" : "Segnale normale" - } - }, - "sr" : { - "stringUnit" : { - "state" : "needs_review", - "value" : "Нормалан сигнал" - } - }, - "zh-Hans" : { - "stringUnit" : { - "state" : "needs_review", - "value" : "信号正常" - } - }, - "zh-Hant-TW" : { - "stringUnit" : { - "state" : "needs_review", - "value" : "訊號正常" - } - } - } - }, - "bluetooth.connected" : { - "comment" : "VoiceOver label for connected Bluetooth icon", - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Connected to Bluetooth device" - } - } - } - }, - "request_position" : { - "comment" : "VoiceOver label for request position button", - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Position anfordern" - } - }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Request position" - } - }, - "it" : { - "stringUnit" : { - "state" : "translated", - "value" : "Richiedi posizione" - } - }, - "sr" : { - "stringUnit" : { - "state" : "translated", - "value" : "Захтевај позицију" - } - }, - "zh-Hans" : { - "stringUnit" : { - "state" : "translated", - "value" : "请求位置" - } - }, - "zh-Hant-TW" : { - "stringUnit" : { - "state" : "translated", - "value" : "請求位置" - } - } - } - }, - "distance" : { - "comment" : "Distance label for VoiceOver" - }, - "device_plugged_in" : { - "comment" : "VoiceOver value for plugged in device", - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Plugged in" - } - } - } - }, - "unknown" : { - "localizations" : { - "it" : { - "stringUnit" : { - "state" : "needs_review", - "value" : "sconosciuto" - } - }, - "sr" : { - "stringUnit" : { - "state" : "needs_review", - "value" : "непознато" - } - }, - "zh-Hant-TW" : { - "stringUnit" : { - "state" : "needs_review", - "value" : "未知" - } - } - } } }, "version" : "1.0" -} +} \ No newline at end of file diff --git a/Meshtastic/Extensions/String.swift b/Meshtastic/Extensions/String.swift index 6a57da9e..d2ae1e5a 100644 --- a/Meshtastic/Extensions/String.swift +++ b/Meshtastic/Extensions/String.swift @@ -115,17 +115,6 @@ extension String { .joined() } - /// Formats a short name like "P130" to read as "Node P 130" for VoiceOver - /// This ensures proper pronunciation of alphanumeric node IDs - func formatNodeNameForVoiceOver() -> String { - let spaced = self.replacingOccurrences( - of: #"([A-Za-z])([0-9]+)"#, - with: "$1 $2", - options: .regularExpression - ) - return "Node " + spaced - } - // Adds variation selectors to prefer the graphical form of emoji. // Looks ahead to make sure that the variation selector is not already applied. var addingVariationSelectors: String { diff --git a/Meshtastic/Views/Bluetooth/Connect.swift b/Meshtastic/Views/Bluetooth/Connect.swift index 32d445bb..5e9dd834 100644 --- a/Meshtastic/Views/Bluetooth/Connect.swift +++ b/Meshtastic/Views/Bluetooth/Connect.swift @@ -12,7 +12,6 @@ import CoreLocation import CoreBluetooth import OSLog import TipKit -import Foundation #if canImport(ActivityKit) import ActivityKit #endif @@ -28,31 +27,6 @@ struct Connect: View { @State var presentingSwitchPreferredPeripheral = false @State var selectedPeripherialId = "" - private func nodeAccessibilityLabel() -> String { - // Create a battery status string that handles charging and plugged in states - var batteryStatus: String? = nil - if let batteryLevel = node?.latestDeviceMetrics?.batteryLevel { - if batteryLevel > 100 { - // Plugged in state - batteryStatus = NSLocalizedString("device_plugged_in", comment: "VoiceOver value for plugged in device") - } else if batteryLevel == 100 { - // Charging state - batteryStatus = NSLocalizedString("device_charging", comment: "VoiceOver value for charging device") - } else { - // Normal battery percentage - batteryStatus = "Battery: \(Int(batteryLevel))%" - } - } - - return [ - node?.user?.shortName?.formatNodeNameForVoiceOver() ?? "", - "BLE Name: \(bleManager.connectedPeripheral?.peripheral.name?.addingVariationSelectors ?? "unknown".localized)", - "Firmware Version: \(node?.metadata?.firmwareVersion ?? "unknown".localized)", - bleManager.isSubscribed ? "Subscribed" : nil, - batteryStatus - ].compactMap { $0 }.joined(separator: ", ") - } - init () { let notificationCenter = UNUserNotificationCenter.current() notificationCenter.getNotificationSettings(completionHandler: { (settings) in @@ -112,8 +86,6 @@ struct Connect: View { } } } - .accessibilityElement(children: .ignore) - .accessibilityLabel(nodeAccessibilityLabel()) .font(.caption) .foregroundColor(Color.gray) .padding([.top]) @@ -327,8 +299,6 @@ struct Connect: View { mqttTopic: bleManager.mqttManager.topic ) } - // Make sure the ZStack passes through accessibility to the ConnectedDevice component - .accessibilityElement(children: .contain) ) } .sheet(isPresented: $invalidFirmwareVersion, onDismiss: didDismissSheet) { diff --git a/Meshtastic/Views/Helpers/BLESignalStrengthIndicator.swift b/Meshtastic/Views/Helpers/BLESignalStrengthIndicator.swift index 73d38f98..c5d17f16 100644 --- a/Meshtastic/Views/Helpers/BLESignalStrengthIndicator.swift +++ b/Meshtastic/Views/Helpers/BLESignalStrengthIndicator.swift @@ -32,65 +32,47 @@ import Foundation import SwiftUI struct SignalStrengthIndicator: View { - // Accessibility: VoiceOver description - private var accessibilityDescription: String { - switch signalStrength { - case .weak: - return NSLocalizedString("ble.signal.strength.weak", comment: "VoiceOver value for weak BLE signal strength") - case .normal: - return NSLocalizedString("ble.signal.strength.normal", comment: "VoiceOver value for normal BLE signal strength") - case .strong: - return NSLocalizedString("ble.signal.strength.strong", comment: "VoiceOver value for strong BLE signal strength") - } - } + let signalStrength: BLESignalStrength - let signalStrength: BLESignalStrength + var body: some View { + HStack { + ForEach(0..<3) { bar in + RoundedRectangle(cornerRadius: 3) + .divided(amount: (CGFloat(bar) + 1) / CGFloat(3)) + .fill(getColor().opacity(bar <= signalStrength.rawValue ? 1 : 0.3)) + .frame(width: 8, height: 40) + } + } + } - var body: some View { - Group { - HStack { - ForEach(0..<3) { bar in - RoundedRectangle(cornerRadius: 3) - .divided(amount: (CGFloat(bar) + 1) / CGFloat(3)) - .fill(getColor().opacity(bar <= signalStrength.rawValue ? 1 : 0.3)) - .frame(width: 8, height: 40) - accessibilityHidden(true) // Ensures bars are ignored - } - } - } - .accessibilityElement(children: .ignore) - .accessibilityLabel(NSLocalizedString("signal_strength", comment: "VoiceOver label for signal strength indicator")) - .accessibilityValue(accessibilityDescription) - } - - private func getColor() -> Color { - switch signalStrength { - case .weak: - return Color.red - case .normal: - return Color.yellow - case .strong: - return Color.green - } - } + private func getColor() -> Color { + switch signalStrength { + case .weak: + return Color.red + case .normal: + return Color.yellow + case .strong: + return Color.green + } + } } struct Divided: Shape { - var amount: CGFloat // Should be in range 0...1 - var shape: S - func path(in rect: CGRect) -> Path { - shape.path(in: rect.divided(atDistance: amount * rect.height, from: .maxYEdge).slice) - } + var amount: CGFloat // Should be in range 0...1 + var shape: S + func path(in rect: CGRect) -> Path { + shape.path(in: rect.divided(atDistance: amount * rect.height, from: .maxYEdge).slice) + } } extension Shape { - func divided(amount: CGFloat) -> Divided { - return Divided(amount: amount, shape: self) - } + func divided(amount: CGFloat) -> Divided { + return Divided(amount: amount, shape: self) + } } enum BLESignalStrength: Int { - case weak = 0 - case normal = 1 - case strong = 2 + case weak = 0 + case normal = 1 + case strong = 2 } diff --git a/Meshtastic/Views/Helpers/BatteryCompact.swift b/Meshtastic/Views/Helpers/BatteryCompact.swift index bb9819a2..4ac61d0c 100644 --- a/Meshtastic/Views/Helpers/BatteryCompact.swift +++ b/Meshtastic/Views/Helpers/BatteryCompact.swift @@ -13,104 +13,69 @@ struct BatteryCompact: View { var color: Color var body: some View { - // Group the battery icon and label in a single accessible container HStack(alignment: .center, spacing: 0) { if let batteryLevel { - // Check for plugged in state - let isPluggedIn = batteryLevel > 100 - let isCharging = batteryLevel == 100 - - // Battery icon selection based on level - if isPluggedIn { - Image(systemName: "powerplug") - .font(iconFont) - .foregroundColor(color) - .symbolRenderingMode(.multicolor) - .accessibilityHidden(true) // Hide from VoiceOver since container will handle it - } else if isCharging { + if batteryLevel == 100 { Image(systemName: "battery.100.bolt") .font(iconFont) .foregroundColor(color) .symbolRenderingMode(.multicolor) - .accessibilityHidden(true) - } else if batteryLevel > 74 { + } else if batteryLevel < 100 && batteryLevel > 74 { Image(systemName: "battery.75") .font(iconFont) .foregroundColor(color) .symbolRenderingMode(.multicolor) - .accessibilityHidden(true) - } else if batteryLevel > 49 { + } else if batteryLevel < 75 && batteryLevel > 49 { Image(systemName: "battery.50") .font(iconFont) .foregroundColor(color) .symbolRenderingMode(.multicolor) - .accessibilityHidden(true) - } else if batteryLevel > 14 { + } else if batteryLevel < 50 && batteryLevel > 14 { Image(systemName: "battery.25") .font(iconFont) .foregroundColor(color) .symbolRenderingMode(.multicolor) - .accessibilityHidden(true) - } else if batteryLevel > 0 { + } else if batteryLevel < 15 && batteryLevel > 0 { Image(systemName: "battery.0") .font(iconFont) .foregroundColor(color) .symbolRenderingMode(.multicolor) - .accessibilityHidden(true) - } else { + } else if batteryLevel == 0 { Image(systemName: "battery.0") .font(iconFont) .foregroundColor(.red) .symbolRenderingMode(.multicolor) - .accessibilityHidden(true) - } - - // Battery text label - if isPluggedIn { - Text("PWD") - .foregroundStyle(.secondary) - .font(font) - .accessibilityHidden(true) - } else if isCharging { - Text("CHG") - .foregroundStyle(.secondary) - .font(font) - .accessibilityHidden(true) - } else { - Text(verbatim: "\(batteryLevel.formatted(.number.precision(.fractionLength(0))))%") - .foregroundStyle(.secondary) - .font(font) - .accessibilityHidden(true) + } else if batteryLevel > 100 { + Image(systemName: "powerplug") + .font(iconFont) + .foregroundColor(color) + .symbolRenderingMode(.multicolor) } } else { - // Unknown battery state Image(systemName: "battery.0") .font(iconFont) .foregroundColor(color) .symbolRenderingMode(.multicolor) - .accessibilityHidden(true) - + } + if let batteryLevel { + if batteryLevel > 100 { + Text("PWD") + .foregroundStyle(.secondary) + .font(font) + } else if batteryLevel == 100 { + Text("CHG") + .foregroundStyle(.secondary) + .font(font) + } else { + Text(verbatim: "\(batteryLevel.formatted(.number.precision(.fractionLength(0))))%") + .foregroundStyle(.secondary) + .font(font) + } + } else { Text(verbatim: "?") .foregroundStyle(.secondary) .font(font) - .accessibilityHidden(true) } } - // Setup container-level accessibility for VoiceOver - .accessibilityElement(children: .ignore) - .accessibilityLabel(NSLocalizedString("Battery Level", comment: "VoiceOver label for battery gauge")) - // Set appropriate value based on the battery state using a computed property - .accessibilityValue(batteryLevel.map { level in - if level > 100 { - // Plugged in - same as PWD visual indicator - return NSLocalizedString("device_plugged_in", comment: "VoiceOver value for plugged in device") - } else if level == 100 { - // Charging - same as CHG visual indicator - return NSLocalizedString("device_charging", comment: "VoiceOver value for charging device") - } else { - // Normal battery level - return String(format: NSLocalizedString("battery_level_percent", comment: "VoiceOver value for battery level"), Int(level)) - } - } ?? "Unknown") } } diff --git a/Meshtastic/Views/Helpers/BatteryGauge.swift b/Meshtastic/Views/Helpers/BatteryGauge.swift index 81e81e7e..952c9768 100644 --- a/Meshtastic/Views/Helpers/BatteryGauge.swift +++ b/Meshtastic/Views/Helpers/BatteryGauge.swift @@ -18,20 +18,18 @@ struct BatteryGauge: View { let deviceMetrics = node.telemetries?.filtered(using: NSPredicate(format: "metricsType == 0")) let mostRecent = deviceMetrics?.lastObject as? TelemetryEntity - // For VoiceOver purposes, detect when device is plugged in (battery > 100%) - let isPluggedIn = (mostRecent?.batteryLevel ?? 0) > 100 - // Use a capped battery level for UI display - let batteryLevel = Double(min(100, mostRecent?.batteryLevel ?? 0)) + let batteryLevel = Double(mostRecent?.batteryLevel ?? 0) VStack { - if isPluggedIn { - // Use a completely standalone view for the plugged in state - // to avoid any VoiceOver confusion - PluggedInIndicator() + if batteryLevel > 100.0 { + // Plugged in + Image(systemName: "powerplug") + .font(.largeTitle) + .foregroundColor(.accentColor) + .symbolRenderingMode(.hierarchical) } else { let gradient = Gradient(colors: [.red, .orange, .green]) Gauge(value: batteryLevel, in: minValue...maxValue) { - // Accessibility for battery gauge if batteryLevel >= 0.0 && batteryLevel < 10 { Label("Battery Level %", systemImage: "battery.0") } else if batteryLevel >= 10.0 && batteryLevel < 25.00 { @@ -52,8 +50,6 @@ struct BatteryGauge: View { Text(Int(batteryLevel), format: .percent) } } - .accessibilityLabel(NSLocalizedString("Battery Level", comment: "VoiceOver label for battery gauge")) - .accessibilityValue(String(format: NSLocalizedString("Battery Level %", comment: "VoiceOver value for battery level"), Int(batteryLevel))) .tint(gradient) .gaugeStyle(.accessoryCircular) } @@ -67,23 +63,6 @@ struct BatteryGauge: View { } } -/// A dedicated view for showing a device is plugged in -/// With proper VoiceOver support that matches the visual indication -struct PluggedInIndicator: View { - var body: some View { - // This view is isolated from any battery measurement - // to ensure VoiceOver doesn't pick up any percentages - Image(systemName: "powerplug") - .font(.largeTitle) - .foregroundColor(.accentColor) - .symbolRenderingMode(.hierarchical) - // Override the accessibility to ensure correct VoiceOver announcement - .accessibilityElement(children: .ignore) - .accessibilityLabel(NSLocalizedString("Battery Level", comment: "VoiceOver label for battery gauge")) - .accessibilityValue(NSLocalizedString("device_plugged_in", comment: "VoiceOver value for plugged in device")) - } -} - struct BatteryGauge_Previews: PreviewProvider { static var previews: some View { VStack { diff --git a/Meshtastic/Views/Helpers/ConnectedDevice.swift b/Meshtastic/Views/Helpers/ConnectedDevice.swift index 4a46db41..c795b1b0 100644 --- a/Meshtastic/Views/Helpers/ConnectedDevice.swift +++ b/Meshtastic/Views/Helpers/ConnectedDevice.swift @@ -21,46 +21,22 @@ struct ConnectedDevice: View { if (phoneOnly && UIDevice.current.userInterfaceIdiom == .phone) || !phoneOnly { if bluetoothOn { if deviceConnected { - // Create an HStack for connected state with proper accessibility - HStack { - if mqttUplinkEnabled || mqttDownlinkEnabled { - MQTTIcon(connected: mqttProxyConnected, uplink: mqttUplinkEnabled, downlink: mqttDownlinkEnabled, topic: mqttTopic) - .accessibilityHidden(true) - } - Image(systemName: "antenna.radiowaves.left.and.right.circle.fill") - .imageScale(.large) - .foregroundColor(.green) - .symbolRenderingMode(.hierarchical) - .accessibilityHidden(true) - Text(name.addingVariationSelectors) - .font(name.isEmoji() ? .title : .callout) - .foregroundColor(.gray) - .accessibilityHidden(true) - } - .accessibilityElement(children: .ignore) - .accessibilityLabel("bluetooth.connected".localized + ", " + name.formatNodeNameForVoiceOver()) + if mqttUplinkEnabled || mqttDownlinkEnabled { + MQTTIcon(connected: mqttProxyConnected, uplink: mqttUplinkEnabled, downlink: mqttDownlinkEnabled, topic: mqttTopic) + } + Image(systemName: "antenna.radiowaves.left.and.right.circle.fill") + .imageScale(.large) + .foregroundColor(.green) + .symbolRenderingMode(.hierarchical) + Text(name.addingVariationSelectors).font(name.isEmoji() ? .title : .callout).foregroundColor(.gray) } else { - // Create a container for disconnected state - HStack { - Image(systemName: "antenna.radiowaves.left.and.right.slash") - .imageScale(.medium) - .foregroundColor(.red) - .symbolRenderingMode(.hierarchical) - .accessibilityHidden(true) - } - .accessibilityElement(children: .ignore) - .accessibilityLabel("bluetooth.not.connected".localized) + Image(systemName: "antenna.radiowaves.left.and.right.slash") + .imageScale(.medium) + .foregroundColor(.red) + .symbolRenderingMode(.hierarchical) } } else { - // Create a container for Bluetooth off state - HStack { - Text("bluetooth.off".localized) - .font(.subheadline) - .foregroundColor(.red) - .accessibilityHidden(true) - } - .accessibilityElement(children: .ignore) - .accessibilityLabel("bluetooth.off".localized) + Text("Bluetooth is off").font(.subheadline).foregroundColor(.red) } } } diff --git a/Meshtastic/Views/Messages/TextMessageField/RequestPositionButton.swift b/Meshtastic/Views/Messages/TextMessageField/RequestPositionButton.swift index fd166f51..2f1634bc 100644 --- a/Meshtastic/Views/Messages/TextMessageField/RequestPositionButton.swift +++ b/Meshtastic/Views/Messages/TextMessageField/RequestPositionButton.swift @@ -6,7 +6,6 @@ struct RequestPositionButton: View { var body: some View { Button(action: action) { Image(systemName: "mappin.and.ellipse") - .accessibilityLabel(NSLocalizedString("request_position", comment: "VoiceOver label for request position button")) .symbolRenderingMode(.hierarchical) .imageScale(.large) .foregroundColor(.accentColor) diff --git a/Meshtastic/Views/Messages/TextMessageField/TextMessageSize.swift b/Meshtastic/Views/Messages/TextMessageField/TextMessageSize.swift index 9839e246..aacbd60d 100644 --- a/Meshtastic/Views/Messages/TextMessageField/TextMessageSize.swift +++ b/Meshtastic/Views/Messages/TextMessageField/TextMessageSize.swift @@ -6,8 +6,6 @@ struct TextMessageSize: View { var body: some View { ProgressView("\("Bytes".localized): \(totalBytes) / \(maxbytes)", value: Double(totalBytes), total: Double(maxbytes)) - .accessibilityLabel(NSLocalizedString("message_size", comment: "VoiceOver label for message size")) - .accessibilityValue(String(format: NSLocalizedString("bytes_used", comment: "VoiceOver value for bytes used"), totalBytes, maxbytes)) .frame(width: 130) .padding(5) .font(.subheadline) diff --git a/Meshtastic/Views/Nodes/Helpers/Actions/IgnoreNodeButton.swift b/Meshtastic/Views/Nodes/Helpers/Actions/IgnoreNodeButton.swift index 2d73d5c0..84fdf4d3 100644 --- a/Meshtastic/Views/Nodes/Helpers/Actions/IgnoreNodeButton.swift +++ b/Meshtastic/Views/Nodes/Helpers/Actions/IgnoreNodeButton.swift @@ -40,7 +40,6 @@ struct IgnoreNodeButton: View { Image(systemName: node.ignored ? "minus.circle.fill" : "minus.circle") .symbolRenderingMode(.multicolor) } - // Accessibility: Label for VoiceOver } } } diff --git a/Meshtastic/Views/Nodes/Helpers/NodeDetail.swift b/Meshtastic/Views/Nodes/Helpers/NodeDetail.swift index c5670e06..081e7adc 100644 --- a/Meshtastic/Views/Nodes/Helpers/NodeDetail.swift +++ b/Meshtastic/Views/Nodes/Helpers/NodeDetail.swift @@ -46,8 +46,7 @@ struct NodeDetail: View { Section("Hardware") { NodeInfoItem(node: node) } - .accessibilityElement(children: .combine) - Section("Node") { // Node + Section("Node") { HStack(alignment: .center) { Spacer() CircleText( @@ -68,7 +67,6 @@ struct NodeDetail: View { .foregroundColor(getRssiColor(rssi: node.rssi)) .font(.caption) } - .accessibilityElement(children: .combine) } if node.telemetries?.count ?? 0 > 0 { Spacer() @@ -76,7 +74,6 @@ struct NodeDetail: View { } Spacer() } - .accessibilityElement(children: .combine) .listRowSeparator(.hidden) if let user = node.user { if !user.keyMatch { @@ -89,7 +86,6 @@ struct NodeDetail: View { .foregroundStyle(.secondary) .font(.callout) } - .accessibilityElement(children: .combine) } icon: { Image(systemName: "key.slash.fill") .symbolRenderingMode(.multicolor) @@ -108,7 +104,6 @@ struct NodeDetail: View { Text(String(node.num)) .textSelection(.enabled) } - .accessibilityElement(children: .combine) HStack { Label { @@ -121,7 +116,6 @@ struct NodeDetail: View { Text(node.num.toHex()) .textSelection(.enabled) } - .accessibilityElement(children: .combine) if let metadata = node.metadata { HStack { @@ -135,7 +129,6 @@ struct NodeDetail: View { Text(metadata.firmwareVersion ?? "Unknown".localized) } - .accessibilityElement(children: .combine) } if let role = node.user?.role, let deviceRole = DeviceRoles(rawValue: Int(role)) { @@ -149,7 +142,6 @@ struct NodeDetail: View { Spacer() Text(deviceRole.name) } - .accessibilityElement(children: .combine) } if let dm = node.telemetries?.filtered(using: NSPredicate(format: "metricsType == 0")).lastObject as? TelemetryEntity, let uptimeSeconds = dm.uptimeSeconds { @@ -169,7 +161,6 @@ struct NodeDetail: View { Text(uptime) .textSelection(.enabled) } - .accessibilityElement(children: .combine) } if let firstHeard = node.firstHeard, firstHeard.timeIntervalSince1970 > 0 && firstHeard < Calendar.current.date(byAdding: .year, value: 1, to: Date())! { @@ -188,9 +179,7 @@ struct NodeDetail: View { Text(firstHeard.formatted()) .textSelection(.enabled) } - } - .accessibilityElement(children: .combine) - .onTapGesture { + }.onTapGesture { dateFormatRelative.toggle() } } @@ -214,9 +203,7 @@ struct NodeDetail: View { Text(lastHeard.formatted()) .textSelection(.enabled) } - } - .accessibilityElement(children: .combine) - .onTapGesture { + }.onTapGesture { dateFormatRelative.toggle() } } @@ -229,84 +216,79 @@ struct NodeDetail: View { if node.hasPositions && UserDefaults.environmentEnableWeatherKit || node.hasDataForLatestEnvironmentMetrics(attributes: ["iaq", "temperature", "relativeHumidity", "barometricPressure", "windSpeed", "radiation", "weight", "Distance", "soilTemperature", "soilMoisture"]) { Section("Environment") { - // Group weather/environment data for better VoiceOver experience - VStack { - if !node.hasEnvironmentMetrics { - LocalWeatherConditions(location: node.latestPosition?.nodeLocation) - } else { - VStack { - if node.latestEnvironmentMetrics?.iaq ?? -1 > 0 { - IndoorAirQuality(iaq: Int(node.latestEnvironmentMetrics?.iaq ?? 0), displayMode: .gradient) - .padding(.vertical) - } - LazyVGrid(columns: gridItemLayout) { - if let temperature = node.latestEnvironmentMetrics?.temperature?.shortFormattedTemperature() { - WeatherConditionsCompactWidget(temperature: String(temperature), symbolName: "cloud.sun", description: "TEMP") - } - if let humidity = node.latestEnvironmentMetrics?.relativeHumidity { - if let temperature = node.latestEnvironmentMetrics?.temperature { - let dewPoint = calculateDewPoint(temp: temperature, relativeHumidity: humidity) - .formatted(.number.precision(.fractionLength(0))) + "°" - HumidityCompactWidget(humidity: Int(humidity), dewPoint: dewPoint) - } else { - HumidityCompactWidget(humidity: Int(humidity), dewPoint: nil) - } - } - if let pressure = node.latestEnvironmentMetrics?.barometricPressure { - PressureCompactWidget(pressure: pressure.formatted(.number.precision(.fractionLength(2))), unit: "hPA", low: pressure <= 1009.144) - } - if let windSpeed = node.latestEnvironmentMetrics?.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)) - 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) - } - if let rainfall1h = node.latestEnvironmentMetrics?.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 - let unitLabel = usesMetricSystem ? "mm" : "in" - let measurement = Measurement(value: Double(rainfall1h), unit: UnitLength.millimeters) - let decimals = usesMetricSystem ? 0 : 1 - 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 { - 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 - let unitLabel = usesMetricSystem ? "mm" : "in" - let measurement = Measurement(value: Double(rainfall24h), unit: UnitLength.millimeters) - let decimals = usesMetricSystem ? 0 : 1 - 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 { - RadiationCompactWidget(radiation: radiation.formatted(.number.precision(.fractionLength(1))), unit: "µR/hr") - } - if let weight = node.latestEnvironmentMetrics?.weight { - WeightCompactWidget(weight: weight.formatted(.number.precision(.fractionLength(1))), unit: "kg") - } - if let distance = node.latestEnvironmentMetrics?.distance { - DistanceCompactWidget(distance: distance.formatted(.number.precision(.fractionLength(0))), unit: "mm") - } - if let soilTemperature = node.latestEnvironmentMetrics?.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 { - SoilMoistureCompactWidget(moisture: soilMoisture.formatted(.number.precision(.fractionLength(0))), unit: "%") - } - } - .padding(node.latestEnvironmentMetrics?.iaq ?? -1 > 0 ? .bottom : .vertical) + if !node.hasEnvironmentMetrics { + LocalWeatherConditions(location: node.latestPosition?.nodeLocation) + } else { + VStack { + if node.latestEnvironmentMetrics?.iaq ?? -1 > 0 { + IndoorAirQuality(iaq: Int(node.latestEnvironmentMetrics?.iaq ?? 0), displayMode: .gradient) + .padding(.vertical) } + LazyVGrid(columns: gridItemLayout) { + if let temperature = node.latestEnvironmentMetrics?.temperature?.shortFormattedTemperature() { + WeatherConditionsCompactWidget(temperature: String(temperature), symbolName: "cloud.sun", description: "TEMP") + } + if let humidity = node.latestEnvironmentMetrics?.relativeHumidity { + if let temperature = node.latestEnvironmentMetrics?.temperature { + let dewPoint = calculateDewPoint(temp: temperature, relativeHumidity: humidity) + .formatted(.number.precision(.fractionLength(0))) + "°" + HumidityCompactWidget(humidity: Int(humidity), dewPoint: dewPoint) + } else { + HumidityCompactWidget(humidity: Int(humidity), dewPoint: nil) + } + } + if let pressure = node.latestEnvironmentMetrics?.barometricPressure { + PressureCompactWidget(pressure: pressure.formatted(.number.precision(.fractionLength(2))), unit: "hPA", low: pressure <= 1009.144) + } + if let windSpeed = node.latestEnvironmentMetrics?.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)) + 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) + } + if let rainfall1h = node.latestEnvironmentMetrics?.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 + let unitLabel = usesMetricSystem ? "mm" : "in" + let measurement = Measurement(value: Double(rainfall1h), unit: UnitLength.millimeters) + let decimals = usesMetricSystem ? 0 : 1 + 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 { + 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 + let unitLabel = usesMetricSystem ? "mm" : "in" + let measurement = Measurement(value: Double(rainfall24h), unit: UnitLength.millimeters) + let decimals = usesMetricSystem ? 0 : 1 + 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 { + RadiationCompactWidget(radiation: radiation.formatted(.number.precision(.fractionLength(1))), unit: "µR/hr") + } + if let weight = node.latestEnvironmentMetrics?.weight { + WeightCompactWidget(weight: weight.formatted(.number.precision(.fractionLength(1))), unit: "kg") + } + if let distance = node.latestEnvironmentMetrics?.distance { + DistanceCompactWidget(distance: distance.formatted(.number.precision(.fractionLength(0))), unit: "mm") + } + if let soilTemperature = node.latestEnvironmentMetrics?.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 { + SoilMoistureCompactWidget(moisture: soilMoisture.formatted(.number.precision(.fractionLength(0))), unit: "%") + } + } + .padding(node.latestEnvironmentMetrics?.iaq ?? -1 > 0 ? .bottom : .vertical) } } - // Apply accessibility properties to the environment section - .accessibilityElement(children: .combine) } } if node.hasPowerMetrics && node.latestPowerMetrics != nil { @@ -316,7 +298,6 @@ struct NodeDetail: View { PowerMetrics(metric: metric) } } - .accessibilityElement(children: .combine) } } Section("Logs") { diff --git a/Meshtastic/Views/Nodes/Helpers/NodeInfoItem.swift b/Meshtastic/Views/Nodes/Helpers/NodeInfoItem.swift index eb4c37b0..07f3d92c 100644 --- a/Meshtastic/Views/Nodes/Helpers/NodeInfoItem.swift +++ b/Meshtastic/Views/Nodes/Helpers/NodeInfoItem.swift @@ -31,7 +31,6 @@ struct NodeInfoItem: View { .foregroundStyle(.gray) .font(.callout) } - .accessibilityElement(children: .combine) Spacer() } VStack(alignment: .center) { @@ -50,11 +49,9 @@ struct NodeInfoItem: View { .cornerRadius(5) } } - .accessibilityElement(children: .combine) } Spacer() } - .accessibilityElement(children: .combine) .onAppear { Api().loadDeviceHardwareData { (hw) in for device in hw { @@ -82,7 +79,6 @@ struct NodeInfoItem: View { Text(String("incomplete".localized)) } } - .accessibilityElement(children: .combine) } } } diff --git a/Meshtastic/Views/Nodes/Helpers/NodeListItem.swift b/Meshtastic/Views/Nodes/Helpers/NodeListItem.swift index 62dd5fd0..2978ceab 100644 --- a/Meshtastic/Views/Nodes/Helpers/NodeListItem.swift +++ b/Meshtastic/Views/Nodes/Helpers/NodeListItem.swift @@ -7,99 +7,9 @@ import SwiftUI import CoreLocation -import Foundation struct NodeListItem: View { - // Accessibility: Synthesized description for VoiceOver - private var accessibilityDescription: String { - var desc = "" - if let shortName = node.user?.shortName { - // Format the shortName using the String extension method - desc = shortName.formatNodeNameForVoiceOver() - } else if let longName = node.user?.longName { - desc = longName - } else { - desc = "unknown node" - } - if connected { - desc += ", currently connected" - } - if node.favorite { - desc += ", favorite" - } - if node.lastHeard != nil { - let formatter = RelativeDateTimeFormatter() - formatter.unitsStyle = .full - let relative = formatter.localizedString(for: node.lastHeard!, relativeTo: Date()) - desc += ", last heard " + relative - } - if node.isOnline { - desc += ", online" - } else { - desc += ", offline" - } - let role = DeviceRoles(rawValue: Int(node.user?.role ?? 0)) - if let roleName = role?.name { - desc += ", role: \(roleName)" - } - if node.hopsAway > 0 { - desc += ", \(node.hopsAway) hops away" - } - if let battery = node.latestDeviceMetrics?.batteryLevel { - // Check for plugged in and charging states, same logic as in BatteryCompact and BatteryGauge - if battery > 100 { - desc += ", " + NSLocalizedString("device_plugged_in", comment: "VoiceOver value for plugged in device") - } else if battery == 100 { - desc += ", " + NSLocalizedString("device_charging", comment: "VoiceOver value for charging device") - } else { - desc += ", battery \(battery)%" - } - } - // Add distance and heading/bearing if available, but only for non-connected nodes - if !connected, let (lastPosition, myCoord) = locationData { - let nodeCoord = CLLocation(latitude: lastPosition.nodeCoordinate!.latitude, longitude: lastPosition.nodeCoordinate!.longitude) - let metersAway = nodeCoord.distance(from: myCoord) - - // Distance information - let distanceFormatter = LengthFormatter() - distanceFormatter.unitStyle = .medium - let formattedDistance = distanceFormatter.string(fromMeters: metersAway) - // For VoiceOver, prepend 'Distance' (localized) - desc += ", " + String(format: "%@: %@", NSLocalizedString("distance", comment: "Distance label for VoiceOver"), formattedDistance) - - // Add bearing/heading information for VoiceOver - let trueBearing = getBearingBetweenTwoPoints(point1: myCoord, point2: nodeCoord) - let heading = Measurement(value: trueBearing, unit: UnitAngle.degrees) - let formattedHeading = heading.formatted(.measurement(width: .narrow, numberFormatStyle: .number.precision(.fractionLength(0)))) - // Using a direct format without requiring a new localization key - desc += ", " + NSLocalizedString("heading", comment: "Heading label for VoiceOver") + " " + formattedHeading - } - // Add signal strength if available - if node.snr != 0 && !node.viaMqtt { - let signalStrength: BLESignalStrength - if node.snr < -10 { - signalStrength = .weak - } else if node.snr < 5 { - signalStrength = .normal - } else { - signalStrength = .strong - } - let signalString: String - switch signalStrength { - case .weak: - signalString = NSLocalizedString("ble.signal.strength.weak", comment: "VoiceOver value for weak BLE signal strength") - case .normal: - signalString = NSLocalizedString("ble.signal.strength.normal", comment: "VoiceOver value for normal BLE signal strength") - case .strong: - signalString = NSLocalizedString("ble.signal.strength.strong", comment: "VoiceOver value for strong BLE signal strength") - } - desc += ", " + signalString - } - return desc - } - - @ObservedObject var node: NodeInfoEntity var connected: Bool var connectedNode: Int64 @@ -257,10 +167,7 @@ struct NodeListItem: View { } .padding(.top, 4) .padding(.bottom, 4) - // Accessibility: Make the whole row a single element for VoiceOver - .accessibilityElement(children: .ignore) - .accessibilityLabel(accessibilityDescription) - } + } } struct DefaultIcon: View { diff --git a/Meshtastic/Views/Nodes/NodeList.swift b/Meshtastic/Views/Nodes/NodeList.swift index 9af36fbd..a17a19d0 100644 --- a/Meshtastic/Views/Nodes/NodeList.swift +++ b/Meshtastic/Views/Nodes/NodeList.swift @@ -235,8 +235,6 @@ struct NodeList: View { phoneOnly: true ) } - // Make sure the ZStack passes through accessibility to the ConnectedDevice component - .accessibilityElement(children: .contain) ) } content: { if let node = selectedNode { @@ -255,7 +253,6 @@ struct NodeList: View { } label: { Image(systemName: "rectangle") } - .accessibilityLabel("Hide sidebar") } ConnectedDevice( bluetoothOn: bleManager.isSwitchedOn, @@ -264,8 +261,6 @@ struct NodeList: View { phoneOnly: true ) } - // Make sure the ZStack passes through accessibility to the ConnectedDevice component - .accessibilityElement(children: .contain) ) } } else { From 916279b0d1bd8dba4783670384342160cf3f2b05 Mon Sep 17 00:00:00 2001 From: Benjamin Faershtein <119711889+RCGV1@users.noreply.github.com> Date: Sun, 11 May 2025 00:00:54 -0700 Subject: [PATCH 016/213] Timestamps above messages spaced by 15 min --- .../Extensions/CoreData/MessageEntityExtension.swift | 7 +++++++ Meshtastic/Views/Messages/ChannelMessageList.swift | 10 ++++++++-- Meshtastic/Views/Messages/UserMessageList.swift | 9 ++++++++- 3 files changed, 23 insertions(+), 3 deletions(-) diff --git a/Meshtastic/Extensions/CoreData/MessageEntityExtension.swift b/Meshtastic/Extensions/CoreData/MessageEntityExtension.swift index 70f36c3d..95071cca 100644 --- a/Meshtastic/Extensions/CoreData/MessageEntityExtension.swift +++ b/Meshtastic/Extensions/CoreData/MessageEntityExtension.swift @@ -31,4 +31,11 @@ extension MessageEntity { return (try? context.fetch(fetchRequest)) ?? [MessageEntity]() } + + func displayTimestamp(aboveMessage: MessageEntity?) -> Bool { + if let aboveMessage = aboveMessage { + return aboveMessage.timestamp.addingTimeInterval(900) < timestamp // 15 seconds + } + return false // First message will have no timestamp + } } diff --git a/Meshtastic/Views/Messages/ChannelMessageList.swift b/Meshtastic/Views/Messages/ChannelMessageList.swift index 6093d9eb..458d20ed 100644 --- a/Meshtastic/Views/Messages/ChannelMessageList.swift +++ b/Meshtastic/Views/Messages/ChannelMessageList.swift @@ -33,8 +33,15 @@ struct ChannelMessageList: View { ZStack(alignment: .bottomTrailing) { ScrollView { LazyVStack { - ForEach(channel.allPrivateMessages) { (message: MessageEntity) in + ForEach(Array(channel.allPrivateMessages.enumerated()), id: \.element.id) { index, message in + // Get the previous message, if it exists + let previousMessage = index > 0 ? channel.allPrivateMessages[index - 1] : nil let currentUser: Bool = (Int64(preferredPeripheralNum) == message.fromUser?.num ? true : false) + if message.displayTimestamp(aboveMessage: previousMessage) { + Text(message.timestamp.formatted(date: .abbreviated, time: .shortened)) + .font(.caption) + .foregroundColor(.gray) + } if message.replyID > 0 { let messageReply = channel.allPrivateMessages.first(where: { $0.messageId == message.replyID }) HStack { @@ -44,7 +51,6 @@ struct ChannelMessageList: View { messageToHighlight = messageNum } scrollView.scrollTo(messageNum, anchor: .center) - // Reset highlight after delay Task { try? await Task.sleep(nanoseconds: 1_000_000_000) // 1 second diff --git a/Meshtastic/Views/Messages/UserMessageList.swift b/Meshtastic/Views/Messages/UserMessageList.swift index 686decaf..54ff0b4d 100644 --- a/Meshtastic/Views/Messages/UserMessageList.swift +++ b/Meshtastic/Views/Messages/UserMessageList.swift @@ -33,7 +33,14 @@ struct UserMessageList: View { ZStack(alignment: .bottomTrailing) { ScrollView { LazyVStack { - ForEach( user.messageList ) { (message: MessageEntity) in + ForEach( Array(user.messageList.enumerated()) , id: \.element.id) { index, message in + // Get the previous message, if it exists + let previousMessage = index > 0 ? user.messageList[index - 1] : nil + if message.displayTimestamp(aboveMessage: previousMessage) { + Text(message.timestamp.formatted(date: .abbreviated, time: .shortened)) + .font(.caption) + .foregroundColor(.gray) + } if user.num != bleManager.connectedPeripheral?.num ?? -1 { let currentUser: Bool = (Int64(UserDefaults.preferredPeripheralNum) == message.fromUser?.num ?? -1 ? true : false) From 5cccdcea22d3af46d4794a2bc7c7d2676d11f7af Mon Sep 17 00:00:00 2001 From: gitbisector Date: Sun, 11 May 2025 16:12:34 -0700 Subject: [PATCH 017/213] Additional accessibilityLabels for VoiceOver users (take #2) --- Localizable.xcstrings | 480 +++++++++++++++++- Meshtastic/Extensions/String.swift | 11 + Meshtastic/Views/Bluetooth/Connect.swift | 29 ++ .../Helpers/BLESignalStrengthIndicator.swift | 82 +-- Meshtastic/Views/Helpers/BatteryCompact.swift | 115 +++-- Meshtastic/Views/Helpers/BatteryGauge.swift | 35 +- .../Views/Helpers/ConnectedDevice.swift | 50 +- .../RequestPositionButton.swift | 1 + .../TextMessageField/TextMessageSize.swift | 2 + .../Helpers/Actions/IgnoreNodeButton.swift | 1 + .../Views/Nodes/Helpers/NodeDetail.swift | 159 +++--- .../Views/Nodes/Helpers/NodeInfoItem.swift | 4 + .../Views/Nodes/Helpers/NodeListItem.swift | 95 +++- Meshtastic/Views/Nodes/NodeList.swift | 5 + 14 files changed, 905 insertions(+), 164 deletions(-) diff --git a/Localizable.xcstrings b/Localizable.xcstrings index aa5cbd4c..3a479de4 100644 --- a/Localizable.xcstrings +++ b/Localizable.xcstrings @@ -35342,7 +35342,485 @@ } } } + }, + "ble.signal.strength.weak" : { + "comment" : "VoiceOver value for weak BLE signal strength", + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "needs_review", + "value" : "Signalstärke schwach" + } + }, + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Signal strength weak" + } + }, + "it" : { + "stringUnit" : { + "state" : "needs_review", + "value" : "Segnale debole" + } + }, + "sr" : { + "stringUnit" : { + "state" : "needs_review", + "value" : "Слаб сигнал" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "needs_review", + "value" : "信号弱" + } + }, + "zh-Hant-TW" : { + "stringUnit" : { + "state" : "needs_review", + "value" : "訊號微弱" + } + } + } + }, + "signal_strength" : { + "comment" : "VoiceOver label for signal strength indicator", + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "needs_review", + "value" : "Signalstärke" + } + }, + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Signal strength" + } + }, + "it" : { + "stringUnit" : { + "state" : "needs_review", + "value" : "Intensità del segnale" + } + }, + "sr" : { + "stringUnit" : { + "state" : "needs_review", + "value" : "Јачина сигнала" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "needs_review", + "value" : "信号强度" + } + }, + "zh-Hant-TW" : { + "stringUnit" : { + "state" : "needs_review", + "value" : "訊號強度" + } + } + } + }, + "message_size" : { + "comment" : "VoiceOver label for message size", + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "needs_review", + "value" : "Nachrichtengröße" + } + }, + "en" : { + "stringUnit" : { + "state" : "needs_review", + "value" : "Message size" + } + }, + "it" : { + "stringUnit" : { + "state" : "needs_review", + "value" : "Dimensione messaggio" + } + }, + "sr" : { + "stringUnit" : { + "state" : "needs_review", + "value" : "Величина поруке" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "needs_review", + "value" : "消息大小" + } + }, + "zh-Hant-TW" : { + "stringUnit" : { + "state" : "needs_review", + "value" : "訊息大小" + } + } + } + }, + "device_charging" : { + "comment" : "VoiceOver value for charging device", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Charging" + } + } + } + }, + "Bluetooth is off.off" : { + "extractionState" : "stale", + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Bluetooth ist aus" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Le Bluetooth est arrêté" + } + }, + "he" : { + "stringUnit" : { + "state" : "translated", + "value" : "בלוטוס כבוי" + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Il Bluetooth è spento" + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Bluetooth jest wyłączony" + } + }, + "se" : { + "stringUnit" : { + "state" : "translated", + "value" : "Bluetooth är avstängt" + } + }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Блутут је искључен" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "蓝牙已关闭" + } + }, + "zh-Hant-TW" : { + "stringUnit" : { + "state" : "translated", + "value" : "藍芽已關閉" + } + } + } + }, + "bytes_used" : { + "comment" : "VoiceOver value for bytes used", + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "needs_review", + "value" : "%d von %d Bytes verwendet" + } + }, + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "%d of %d bytes used" + } + }, + "it" : { + "stringUnit" : { + "state" : "needs_review", + "value" : "%d di %d byte usati" + } + }, + "sr" : { + "stringUnit" : { + "state" : "needs_review", + "value" : "%d од %d бајтова искоришћено" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "needs_review", + "value" : "已用%d/%d字节" + } + }, + "zh-Hant-TW" : { + "stringUnit" : { + "state" : "needs_review", + "value" : "已用%d/%d位元組" + } + } + } + }, + "heading" : { + "comment" : "Heading label for VoiceOver" + }, + "Hide sidebar" : {}, + "bluetooth.not.connected" : { + "comment" : "VoiceOver label for disconnected Bluetooth icon", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "No Bluetooth device connected" + } + } + } + }, + "device.configuration" : { + "extractionState" : "stale", + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "needs_review", + "value" : "Gerätekonfiguration" + } + }, + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Device Configuration" + } + }, + "he" : { + "stringUnit" : { + "state" : "needs_review", + "value" : "Device Configuration" + } + }, + "it" : { + "stringUnit" : { + "state" : "needs_review", + "value" : "Configurazione del dispositivo" + } + }, + "pl" : { + "stringUnit" : { + "state" : "needs_review", + "value" : "Device Configuration" + } + }, + "se" : { + "stringUnit" : { + "state" : "needs_review", + "value" : "Enhetsinställningar" + } + }, + "sr" : { + "stringUnit" : { + "state" : "needs_review", + "value" : "Подешавања уређаја" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "needs_review", + "value" : "设备配置" + } + }, + "zh-Hant-TW" : { + "stringUnit" : { + "state" : "needs_review", + "value" : "設備設定" + } + } + } + }, + "ble.signal.strength.strong" : { + "comment" : "VoiceOver value for strong BLE signal strength", + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "needs_review", + "value" : "Signalstärke stark" + } + }, + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Signal strength strong" + } + }, + "it" : { + "stringUnit" : { + "state" : "needs_review", + "value" : "Segnale forte" + } + }, + "sr" : { + "stringUnit" : { + "state" : "needs_review", + "value" : "Јак сигнал" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "needs_review", + "value" : "信号强" + } + }, + "zh-Hant-TW" : { + "stringUnit" : { + "state" : "needs_review", + "value" : "訊號強" + } + } + } + }, + "ble.signal.strength.normal" : { + "comment" : "VoiceOver value for normal BLE signal strength", + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "needs_review", + "value" : "Signalstärke normal" + } + }, + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Signal strength normal" + } + }, + "it" : { + "stringUnit" : { + "state" : "needs_review", + "value" : "Segnale normale" + } + }, + "sr" : { + "stringUnit" : { + "state" : "needs_review", + "value" : "Нормалан сигнал" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "needs_review", + "value" : "信号正常" + } + }, + "zh-Hant-TW" : { + "stringUnit" : { + "state" : "needs_review", + "value" : "訊號正常" + } + } + } + }, + "bluetooth.connected" : { + "comment" : "VoiceOver label for connected Bluetooth icon", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Connected to Bluetooth device" + } + } + } + }, + "request_position" : { + "comment" : "VoiceOver label for request position button", + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Position anfordern" + } + }, + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Request position" + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Richiedi posizione" + } + }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Захтевај позицију" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "请求位置" + } + }, + "zh-Hant-TW" : { + "stringUnit" : { + "state" : "translated", + "value" : "請求位置" + } + } + } + }, + "distance" : { + "comment" : "Distance label for VoiceOver" + }, + "device_plugged_in" : { + "comment" : "VoiceOver value for plugged in device", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Plugged in" + } + } + } + }, + "unknown" : { + "localizations" : { + "it" : { + "stringUnit" : { + "state" : "needs_review", + "value" : "sconosciuto" + } + }, + "sr" : { + "stringUnit" : { + "state" : "needs_review", + "value" : "непознато" + } + }, + "zh-Hant-TW" : { + "stringUnit" : { + "state" : "needs_review", + "value" : "未知" + } + } + } } }, "version" : "1.0" -} \ No newline at end of file +} diff --git a/Meshtastic/Extensions/String.swift b/Meshtastic/Extensions/String.swift index d2ae1e5a..6a57da9e 100644 --- a/Meshtastic/Extensions/String.swift +++ b/Meshtastic/Extensions/String.swift @@ -115,6 +115,17 @@ extension String { .joined() } + /// Formats a short name like "P130" to read as "Node P 130" for VoiceOver + /// This ensures proper pronunciation of alphanumeric node IDs + func formatNodeNameForVoiceOver() -> String { + let spaced = self.replacingOccurrences( + of: #"([A-Za-z])([0-9]+)"#, + with: "$1 $2", + options: .regularExpression + ) + return "Node " + spaced + } + // Adds variation selectors to prefer the graphical form of emoji. // Looks ahead to make sure that the variation selector is not already applied. var addingVariationSelectors: String { diff --git a/Meshtastic/Views/Bluetooth/Connect.swift b/Meshtastic/Views/Bluetooth/Connect.swift index 5e9dd834..60bb2072 100644 --- a/Meshtastic/Views/Bluetooth/Connect.swift +++ b/Meshtastic/Views/Bluetooth/Connect.swift @@ -27,6 +27,31 @@ struct Connect: View { @State var presentingSwitchPreferredPeripheral = false @State var selectedPeripherialId = "" + private func nodeAccessibilityLabel() -> String { + // Create a battery status string that handles charging and plugged in states + var batteryStatus: String? = nil + if let batteryLevel = node?.latestDeviceMetrics?.batteryLevel { + if batteryLevel > 100 { + // Plugged in state + batteryStatus = NSLocalizedString("device_plugged_in", comment: "VoiceOver value for plugged in device") + } else if batteryLevel == 100 { + // Charging state + batteryStatus = NSLocalizedString("device_charging", comment: "VoiceOver value for charging device") + } else { + // Normal battery percentage + batteryStatus = "Battery: \(Int(batteryLevel))%" + } + } + + return [ + node?.user?.shortName?.formatNodeNameForVoiceOver() ?? "", + "BLE Name: \(bleManager.connectedPeripheral?.peripheral.name?.addingVariationSelectors ?? "unknown".localized)", + "Firmware Version: \(node?.metadata?.firmwareVersion ?? "unknown".localized)", + bleManager.isSubscribed ? "Subscribed" : nil, + batteryStatus + ].compactMap { $0 }.joined(separator: ", ") + } + init () { let notificationCenter = UNUserNotificationCenter.current() notificationCenter.getNotificationSettings(completionHandler: { (settings) in @@ -86,6 +111,8 @@ struct Connect: View { } } } + .accessibilityElement(children: .ignore) + .accessibilityLabel(nodeAccessibilityLabel()) .font(.caption) .foregroundColor(Color.gray) .padding([.top]) @@ -299,6 +326,8 @@ struct Connect: View { mqttTopic: bleManager.mqttManager.topic ) } + // Make sure the ZStack passes through accessibility to the ConnectedDevice component + .accessibilityElement(children: .contain) ) } .sheet(isPresented: $invalidFirmwareVersion, onDismiss: didDismissSheet) { diff --git a/Meshtastic/Views/Helpers/BLESignalStrengthIndicator.swift b/Meshtastic/Views/Helpers/BLESignalStrengthIndicator.swift index c5d17f16..73d38f98 100644 --- a/Meshtastic/Views/Helpers/BLESignalStrengthIndicator.swift +++ b/Meshtastic/Views/Helpers/BLESignalStrengthIndicator.swift @@ -32,47 +32,65 @@ import Foundation import SwiftUI struct SignalStrengthIndicator: View { - let signalStrength: BLESignalStrength + // Accessibility: VoiceOver description + private var accessibilityDescription: String { + switch signalStrength { + case .weak: + return NSLocalizedString("ble.signal.strength.weak", comment: "VoiceOver value for weak BLE signal strength") + case .normal: + return NSLocalizedString("ble.signal.strength.normal", comment: "VoiceOver value for normal BLE signal strength") + case .strong: + return NSLocalizedString("ble.signal.strength.strong", comment: "VoiceOver value for strong BLE signal strength") + } + } - var body: some View { - HStack { - ForEach(0..<3) { bar in - RoundedRectangle(cornerRadius: 3) - .divided(amount: (CGFloat(bar) + 1) / CGFloat(3)) - .fill(getColor().opacity(bar <= signalStrength.rawValue ? 1 : 0.3)) - .frame(width: 8, height: 40) - } - } - } + let signalStrength: BLESignalStrength - private func getColor() -> Color { - switch signalStrength { - case .weak: - return Color.red - case .normal: - return Color.yellow - case .strong: - return Color.green - } - } + var body: some View { + Group { + HStack { + ForEach(0..<3) { bar in + RoundedRectangle(cornerRadius: 3) + .divided(amount: (CGFloat(bar) + 1) / CGFloat(3)) + .fill(getColor().opacity(bar <= signalStrength.rawValue ? 1 : 0.3)) + .frame(width: 8, height: 40) + accessibilityHidden(true) // Ensures bars are ignored + } + } + } + .accessibilityElement(children: .ignore) + .accessibilityLabel(NSLocalizedString("signal_strength", comment: "VoiceOver label for signal strength indicator")) + .accessibilityValue(accessibilityDescription) + } + + private func getColor() -> Color { + switch signalStrength { + case .weak: + return Color.red + case .normal: + return Color.yellow + case .strong: + return Color.green + } + } } struct Divided: Shape { - var amount: CGFloat // Should be in range 0...1 - var shape: S - func path(in rect: CGRect) -> Path { - shape.path(in: rect.divided(atDistance: amount * rect.height, from: .maxYEdge).slice) - } + var amount: CGFloat // Should be in range 0...1 + var shape: S + func path(in rect: CGRect) -> Path { + shape.path(in: rect.divided(atDistance: amount * rect.height, from: .maxYEdge).slice) + } } extension Shape { - func divided(amount: CGFloat) -> Divided { - return Divided(amount: amount, shape: self) - } + func divided(amount: CGFloat) -> Divided { + return Divided(amount: amount, shape: self) + } } enum BLESignalStrength: Int { - case weak = 0 - case normal = 1 - case strong = 2 + case weak = 0 + case normal = 1 + case strong = 2 } diff --git a/Meshtastic/Views/Helpers/BatteryCompact.swift b/Meshtastic/Views/Helpers/BatteryCompact.swift index 4ac61d0c..bb9819a2 100644 --- a/Meshtastic/Views/Helpers/BatteryCompact.swift +++ b/Meshtastic/Views/Helpers/BatteryCompact.swift @@ -13,69 +13,104 @@ struct BatteryCompact: View { var color: Color var body: some View { + // Group the battery icon and label in a single accessible container HStack(alignment: .center, spacing: 0) { if let batteryLevel { - if batteryLevel == 100 { - Image(systemName: "battery.100.bolt") - .font(iconFont) - .foregroundColor(color) - .symbolRenderingMode(.multicolor) - } else if batteryLevel < 100 && batteryLevel > 74 { - Image(systemName: "battery.75") - .font(iconFont) - .foregroundColor(color) - .symbolRenderingMode(.multicolor) - } else if batteryLevel < 75 && batteryLevel > 49 { - Image(systemName: "battery.50") - .font(iconFont) - .foregroundColor(color) - .symbolRenderingMode(.multicolor) - } else if batteryLevel < 50 && batteryLevel > 14 { - Image(systemName: "battery.25") - .font(iconFont) - .foregroundColor(color) - .symbolRenderingMode(.multicolor) - } else if batteryLevel < 15 && batteryLevel > 0 { - Image(systemName: "battery.0") - .font(iconFont) - .foregroundColor(color) - .symbolRenderingMode(.multicolor) - } else if batteryLevel == 0 { - Image(systemName: "battery.0") - .font(iconFont) - .foregroundColor(.red) - .symbolRenderingMode(.multicolor) - } else if batteryLevel > 100 { + // Check for plugged in state + let isPluggedIn = batteryLevel > 100 + let isCharging = batteryLevel == 100 + + // Battery icon selection based on level + if isPluggedIn { Image(systemName: "powerplug") .font(iconFont) .foregroundColor(color) .symbolRenderingMode(.multicolor) + .accessibilityHidden(true) // Hide from VoiceOver since container will handle it + } else if isCharging { + Image(systemName: "battery.100.bolt") + .font(iconFont) + .foregroundColor(color) + .symbolRenderingMode(.multicolor) + .accessibilityHidden(true) + } else if batteryLevel > 74 { + Image(systemName: "battery.75") + .font(iconFont) + .foregroundColor(color) + .symbolRenderingMode(.multicolor) + .accessibilityHidden(true) + } else if batteryLevel > 49 { + Image(systemName: "battery.50") + .font(iconFont) + .foregroundColor(color) + .symbolRenderingMode(.multicolor) + .accessibilityHidden(true) + } else if batteryLevel > 14 { + Image(systemName: "battery.25") + .font(iconFont) + .foregroundColor(color) + .symbolRenderingMode(.multicolor) + .accessibilityHidden(true) + } else if batteryLevel > 0 { + Image(systemName: "battery.0") + .font(iconFont) + .foregroundColor(color) + .symbolRenderingMode(.multicolor) + .accessibilityHidden(true) + } else { + Image(systemName: "battery.0") + .font(iconFont) + .foregroundColor(.red) + .symbolRenderingMode(.multicolor) + .accessibilityHidden(true) } - } else { - Image(systemName: "battery.0") - .font(iconFont) - .foregroundColor(color) - .symbolRenderingMode(.multicolor) - } - if let batteryLevel { - if batteryLevel > 100 { + + // Battery text label + if isPluggedIn { Text("PWD") .foregroundStyle(.secondary) .font(font) - } else if batteryLevel == 100 { + .accessibilityHidden(true) + } else if isCharging { Text("CHG") .foregroundStyle(.secondary) .font(font) + .accessibilityHidden(true) } else { Text(verbatim: "\(batteryLevel.formatted(.number.precision(.fractionLength(0))))%") .foregroundStyle(.secondary) .font(font) + .accessibilityHidden(true) } } else { + // Unknown battery state + Image(systemName: "battery.0") + .font(iconFont) + .foregroundColor(color) + .symbolRenderingMode(.multicolor) + .accessibilityHidden(true) + Text(verbatim: "?") .foregroundStyle(.secondary) .font(font) + .accessibilityHidden(true) } } + // Setup container-level accessibility for VoiceOver + .accessibilityElement(children: .ignore) + .accessibilityLabel(NSLocalizedString("Battery Level", comment: "VoiceOver label for battery gauge")) + // Set appropriate value based on the battery state using a computed property + .accessibilityValue(batteryLevel.map { level in + if level > 100 { + // Plugged in - same as PWD visual indicator + return NSLocalizedString("device_plugged_in", comment: "VoiceOver value for plugged in device") + } else if level == 100 { + // Charging - same as CHG visual indicator + return NSLocalizedString("device_charging", comment: "VoiceOver value for charging device") + } else { + // Normal battery level + return String(format: NSLocalizedString("battery_level_percent", comment: "VoiceOver value for battery level"), Int(level)) + } + } ?? "Unknown") } } diff --git a/Meshtastic/Views/Helpers/BatteryGauge.swift b/Meshtastic/Views/Helpers/BatteryGauge.swift index 952c9768..81e81e7e 100644 --- a/Meshtastic/Views/Helpers/BatteryGauge.swift +++ b/Meshtastic/Views/Helpers/BatteryGauge.swift @@ -18,18 +18,20 @@ struct BatteryGauge: View { let deviceMetrics = node.telemetries?.filtered(using: NSPredicate(format: "metricsType == 0")) let mostRecent = deviceMetrics?.lastObject as? TelemetryEntity - let batteryLevel = Double(mostRecent?.batteryLevel ?? 0) + // For VoiceOver purposes, detect when device is plugged in (battery > 100%) + let isPluggedIn = (mostRecent?.batteryLevel ?? 0) > 100 + // Use a capped battery level for UI display + let batteryLevel = Double(min(100, mostRecent?.batteryLevel ?? 0)) VStack { - if batteryLevel > 100.0 { - // Plugged in - Image(systemName: "powerplug") - .font(.largeTitle) - .foregroundColor(.accentColor) - .symbolRenderingMode(.hierarchical) + if isPluggedIn { + // Use a completely standalone view for the plugged in state + // to avoid any VoiceOver confusion + PluggedInIndicator() } else { let gradient = Gradient(colors: [.red, .orange, .green]) Gauge(value: batteryLevel, in: minValue...maxValue) { + // Accessibility for battery gauge if batteryLevel >= 0.0 && batteryLevel < 10 { Label("Battery Level %", systemImage: "battery.0") } else if batteryLevel >= 10.0 && batteryLevel < 25.00 { @@ -50,6 +52,8 @@ struct BatteryGauge: View { Text(Int(batteryLevel), format: .percent) } } + .accessibilityLabel(NSLocalizedString("Battery Level", comment: "VoiceOver label for battery gauge")) + .accessibilityValue(String(format: NSLocalizedString("Battery Level %", comment: "VoiceOver value for battery level"), Int(batteryLevel))) .tint(gradient) .gaugeStyle(.accessoryCircular) } @@ -63,6 +67,23 @@ struct BatteryGauge: View { } } +/// A dedicated view for showing a device is plugged in +/// With proper VoiceOver support that matches the visual indication +struct PluggedInIndicator: View { + var body: some View { + // This view is isolated from any battery measurement + // to ensure VoiceOver doesn't pick up any percentages + Image(systemName: "powerplug") + .font(.largeTitle) + .foregroundColor(.accentColor) + .symbolRenderingMode(.hierarchical) + // Override the accessibility to ensure correct VoiceOver announcement + .accessibilityElement(children: .ignore) + .accessibilityLabel(NSLocalizedString("Battery Level", comment: "VoiceOver label for battery gauge")) + .accessibilityValue(NSLocalizedString("device_plugged_in", comment: "VoiceOver value for plugged in device")) + } +} + struct BatteryGauge_Previews: PreviewProvider { static var previews: some View { VStack { diff --git a/Meshtastic/Views/Helpers/ConnectedDevice.swift b/Meshtastic/Views/Helpers/ConnectedDevice.swift index c795b1b0..4a46db41 100644 --- a/Meshtastic/Views/Helpers/ConnectedDevice.swift +++ b/Meshtastic/Views/Helpers/ConnectedDevice.swift @@ -21,22 +21,46 @@ struct ConnectedDevice: View { if (phoneOnly && UIDevice.current.userInterfaceIdiom == .phone) || !phoneOnly { if bluetoothOn { if deviceConnected { - if mqttUplinkEnabled || mqttDownlinkEnabled { - MQTTIcon(connected: mqttProxyConnected, uplink: mqttUplinkEnabled, downlink: mqttDownlinkEnabled, topic: mqttTopic) - } - Image(systemName: "antenna.radiowaves.left.and.right.circle.fill") - .imageScale(.large) - .foregroundColor(.green) - .symbolRenderingMode(.hierarchical) - Text(name.addingVariationSelectors).font(name.isEmoji() ? .title : .callout).foregroundColor(.gray) + // Create an HStack for connected state with proper accessibility + HStack { + if mqttUplinkEnabled || mqttDownlinkEnabled { + MQTTIcon(connected: mqttProxyConnected, uplink: mqttUplinkEnabled, downlink: mqttDownlinkEnabled, topic: mqttTopic) + .accessibilityHidden(true) + } + Image(systemName: "antenna.radiowaves.left.and.right.circle.fill") + .imageScale(.large) + .foregroundColor(.green) + .symbolRenderingMode(.hierarchical) + .accessibilityHidden(true) + Text(name.addingVariationSelectors) + .font(name.isEmoji() ? .title : .callout) + .foregroundColor(.gray) + .accessibilityHidden(true) + } + .accessibilityElement(children: .ignore) + .accessibilityLabel("bluetooth.connected".localized + ", " + name.formatNodeNameForVoiceOver()) } else { - Image(systemName: "antenna.radiowaves.left.and.right.slash") - .imageScale(.medium) - .foregroundColor(.red) - .symbolRenderingMode(.hierarchical) + // Create a container for disconnected state + HStack { + Image(systemName: "antenna.radiowaves.left.and.right.slash") + .imageScale(.medium) + .foregroundColor(.red) + .symbolRenderingMode(.hierarchical) + .accessibilityHidden(true) + } + .accessibilityElement(children: .ignore) + .accessibilityLabel("bluetooth.not.connected".localized) } } else { - Text("Bluetooth is off").font(.subheadline).foregroundColor(.red) + // Create a container for Bluetooth off state + HStack { + Text("bluetooth.off".localized) + .font(.subheadline) + .foregroundColor(.red) + .accessibilityHidden(true) + } + .accessibilityElement(children: .ignore) + .accessibilityLabel("bluetooth.off".localized) } } } diff --git a/Meshtastic/Views/Messages/TextMessageField/RequestPositionButton.swift b/Meshtastic/Views/Messages/TextMessageField/RequestPositionButton.swift index 2f1634bc..fd166f51 100644 --- a/Meshtastic/Views/Messages/TextMessageField/RequestPositionButton.swift +++ b/Meshtastic/Views/Messages/TextMessageField/RequestPositionButton.swift @@ -6,6 +6,7 @@ struct RequestPositionButton: View { var body: some View { Button(action: action) { Image(systemName: "mappin.and.ellipse") + .accessibilityLabel(NSLocalizedString("request_position", comment: "VoiceOver label for request position button")) .symbolRenderingMode(.hierarchical) .imageScale(.large) .foregroundColor(.accentColor) diff --git a/Meshtastic/Views/Messages/TextMessageField/TextMessageSize.swift b/Meshtastic/Views/Messages/TextMessageField/TextMessageSize.swift index aacbd60d..9839e246 100644 --- a/Meshtastic/Views/Messages/TextMessageField/TextMessageSize.swift +++ b/Meshtastic/Views/Messages/TextMessageField/TextMessageSize.swift @@ -6,6 +6,8 @@ struct TextMessageSize: View { var body: some View { ProgressView("\("Bytes".localized): \(totalBytes) / \(maxbytes)", value: Double(totalBytes), total: Double(maxbytes)) + .accessibilityLabel(NSLocalizedString("message_size", comment: "VoiceOver label for message size")) + .accessibilityValue(String(format: NSLocalizedString("bytes_used", comment: "VoiceOver value for bytes used"), totalBytes, maxbytes)) .frame(width: 130) .padding(5) .font(.subheadline) diff --git a/Meshtastic/Views/Nodes/Helpers/Actions/IgnoreNodeButton.swift b/Meshtastic/Views/Nodes/Helpers/Actions/IgnoreNodeButton.swift index 84fdf4d3..2d73d5c0 100644 --- a/Meshtastic/Views/Nodes/Helpers/Actions/IgnoreNodeButton.swift +++ b/Meshtastic/Views/Nodes/Helpers/Actions/IgnoreNodeButton.swift @@ -40,6 +40,7 @@ struct IgnoreNodeButton: View { Image(systemName: node.ignored ? "minus.circle.fill" : "minus.circle") .symbolRenderingMode(.multicolor) } + // Accessibility: Label for VoiceOver } } } diff --git a/Meshtastic/Views/Nodes/Helpers/NodeDetail.swift b/Meshtastic/Views/Nodes/Helpers/NodeDetail.swift index 081e7adc..c5670e06 100644 --- a/Meshtastic/Views/Nodes/Helpers/NodeDetail.swift +++ b/Meshtastic/Views/Nodes/Helpers/NodeDetail.swift @@ -46,7 +46,8 @@ struct NodeDetail: View { Section("Hardware") { NodeInfoItem(node: node) } - Section("Node") { + .accessibilityElement(children: .combine) + Section("Node") { // Node HStack(alignment: .center) { Spacer() CircleText( @@ -67,6 +68,7 @@ struct NodeDetail: View { .foregroundColor(getRssiColor(rssi: node.rssi)) .font(.caption) } + .accessibilityElement(children: .combine) } if node.telemetries?.count ?? 0 > 0 { Spacer() @@ -74,6 +76,7 @@ struct NodeDetail: View { } Spacer() } + .accessibilityElement(children: .combine) .listRowSeparator(.hidden) if let user = node.user { if !user.keyMatch { @@ -86,6 +89,7 @@ struct NodeDetail: View { .foregroundStyle(.secondary) .font(.callout) } + .accessibilityElement(children: .combine) } icon: { Image(systemName: "key.slash.fill") .symbolRenderingMode(.multicolor) @@ -104,6 +108,7 @@ struct NodeDetail: View { Text(String(node.num)) .textSelection(.enabled) } + .accessibilityElement(children: .combine) HStack { Label { @@ -116,6 +121,7 @@ struct NodeDetail: View { Text(node.num.toHex()) .textSelection(.enabled) } + .accessibilityElement(children: .combine) if let metadata = node.metadata { HStack { @@ -129,6 +135,7 @@ struct NodeDetail: View { Text(metadata.firmwareVersion ?? "Unknown".localized) } + .accessibilityElement(children: .combine) } if let role = node.user?.role, let deviceRole = DeviceRoles(rawValue: Int(role)) { @@ -142,6 +149,7 @@ struct NodeDetail: View { Spacer() Text(deviceRole.name) } + .accessibilityElement(children: .combine) } if let dm = node.telemetries?.filtered(using: NSPredicate(format: "metricsType == 0")).lastObject as? TelemetryEntity, let uptimeSeconds = dm.uptimeSeconds { @@ -161,6 +169,7 @@ struct NodeDetail: View { Text(uptime) .textSelection(.enabled) } + .accessibilityElement(children: .combine) } if let firstHeard = node.firstHeard, firstHeard.timeIntervalSince1970 > 0 && firstHeard < Calendar.current.date(byAdding: .year, value: 1, to: Date())! { @@ -179,7 +188,9 @@ struct NodeDetail: View { Text(firstHeard.formatted()) .textSelection(.enabled) } - }.onTapGesture { + } + .accessibilityElement(children: .combine) + .onTapGesture { dateFormatRelative.toggle() } } @@ -203,7 +214,9 @@ struct NodeDetail: View { Text(lastHeard.formatted()) .textSelection(.enabled) } - }.onTapGesture { + } + .accessibilityElement(children: .combine) + .onTapGesture { dateFormatRelative.toggle() } } @@ -216,79 +229,84 @@ struct NodeDetail: View { if node.hasPositions && UserDefaults.environmentEnableWeatherKit || node.hasDataForLatestEnvironmentMetrics(attributes: ["iaq", "temperature", "relativeHumidity", "barometricPressure", "windSpeed", "radiation", "weight", "Distance", "soilTemperature", "soilMoisture"]) { Section("Environment") { - if !node.hasEnvironmentMetrics { - LocalWeatherConditions(location: node.latestPosition?.nodeLocation) - } else { - VStack { - if node.latestEnvironmentMetrics?.iaq ?? -1 > 0 { - IndoorAirQuality(iaq: Int(node.latestEnvironmentMetrics?.iaq ?? 0), displayMode: .gradient) - .padding(.vertical) - } - LazyVGrid(columns: gridItemLayout) { - if let temperature = node.latestEnvironmentMetrics?.temperature?.shortFormattedTemperature() { - WeatherConditionsCompactWidget(temperature: String(temperature), symbolName: "cloud.sun", description: "TEMP") + // Group weather/environment data for better VoiceOver experience + VStack { + if !node.hasEnvironmentMetrics { + LocalWeatherConditions(location: node.latestPosition?.nodeLocation) + } else { + VStack { + if node.latestEnvironmentMetrics?.iaq ?? -1 > 0 { + IndoorAirQuality(iaq: Int(node.latestEnvironmentMetrics?.iaq ?? 0), displayMode: .gradient) + .padding(.vertical) } - if let humidity = node.latestEnvironmentMetrics?.relativeHumidity { - if let temperature = node.latestEnvironmentMetrics?.temperature { - let dewPoint = calculateDewPoint(temp: temperature, relativeHumidity: humidity) - .formatted(.number.precision(.fractionLength(0))) + "°" - HumidityCompactWidget(humidity: Int(humidity), dewPoint: dewPoint) - } else { - HumidityCompactWidget(humidity: Int(humidity), dewPoint: nil) + LazyVGrid(columns: gridItemLayout) { + if let temperature = node.latestEnvironmentMetrics?.temperature?.shortFormattedTemperature() { + WeatherConditionsCompactWidget(temperature: String(temperature), symbolName: "cloud.sun", description: "TEMP") + } + if let humidity = node.latestEnvironmentMetrics?.relativeHumidity { + if let temperature = node.latestEnvironmentMetrics?.temperature { + let dewPoint = calculateDewPoint(temp: temperature, relativeHumidity: humidity) + .formatted(.number.precision(.fractionLength(0))) + "°" + HumidityCompactWidget(humidity: Int(humidity), dewPoint: dewPoint) + } else { + HumidityCompactWidget(humidity: Int(humidity), dewPoint: nil) + } + } + if let pressure = node.latestEnvironmentMetrics?.barometricPressure { + PressureCompactWidget(pressure: pressure.formatted(.number.precision(.fractionLength(2))), unit: "hPA", low: pressure <= 1009.144) + } + if let windSpeed = node.latestEnvironmentMetrics?.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)) + 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) + } + if let rainfall1h = node.latestEnvironmentMetrics?.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 + let unitLabel = usesMetricSystem ? "mm" : "in" + let measurement = Measurement(value: Double(rainfall1h), unit: UnitLength.millimeters) + let decimals = usesMetricSystem ? 0 : 1 + 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 { + 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 + let unitLabel = usesMetricSystem ? "mm" : "in" + let measurement = Measurement(value: Double(rainfall24h), unit: UnitLength.millimeters) + let decimals = usesMetricSystem ? 0 : 1 + 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 { + RadiationCompactWidget(radiation: radiation.formatted(.number.precision(.fractionLength(1))), unit: "µR/hr") + } + if let weight = node.latestEnvironmentMetrics?.weight { + WeightCompactWidget(weight: weight.formatted(.number.precision(.fractionLength(1))), unit: "kg") + } + if let distance = node.latestEnvironmentMetrics?.distance { + DistanceCompactWidget(distance: distance.formatted(.number.precision(.fractionLength(0))), unit: "mm") + } + if let soilTemperature = node.latestEnvironmentMetrics?.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 { + SoilMoistureCompactWidget(moisture: soilMoisture.formatted(.number.precision(.fractionLength(0))), unit: "%") } } - if let pressure = node.latestEnvironmentMetrics?.barometricPressure { - PressureCompactWidget(pressure: pressure.formatted(.number.precision(.fractionLength(2))), unit: "hPA", low: pressure <= 1009.144) - } - if let windSpeed = node.latestEnvironmentMetrics?.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)) - 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) - } - if let rainfall1h = node.latestEnvironmentMetrics?.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 - let unitLabel = usesMetricSystem ? "mm" : "in" - let measurement = Measurement(value: Double(rainfall1h), unit: UnitLength.millimeters) - let decimals = usesMetricSystem ? 0 : 1 - 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 { - 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 - let unitLabel = usesMetricSystem ? "mm" : "in" - let measurement = Measurement(value: Double(rainfall24h), unit: UnitLength.millimeters) - let decimals = usesMetricSystem ? 0 : 1 - 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 { - RadiationCompactWidget(radiation: radiation.formatted(.number.precision(.fractionLength(1))), unit: "µR/hr") - } - if let weight = node.latestEnvironmentMetrics?.weight { - WeightCompactWidget(weight: weight.formatted(.number.precision(.fractionLength(1))), unit: "kg") - } - if let distance = node.latestEnvironmentMetrics?.distance { - DistanceCompactWidget(distance: distance.formatted(.number.precision(.fractionLength(0))), unit: "mm") - } - if let soilTemperature = node.latestEnvironmentMetrics?.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 { - SoilMoistureCompactWidget(moisture: soilMoisture.formatted(.number.precision(.fractionLength(0))), unit: "%") - } + .padding(node.latestEnvironmentMetrics?.iaq ?? -1 > 0 ? .bottom : .vertical) } - .padding(node.latestEnvironmentMetrics?.iaq ?? -1 > 0 ? .bottom : .vertical) } } + // Apply accessibility properties to the environment section + .accessibilityElement(children: .combine) } } if node.hasPowerMetrics && node.latestPowerMetrics != nil { @@ -298,6 +316,7 @@ struct NodeDetail: View { PowerMetrics(metric: metric) } } + .accessibilityElement(children: .combine) } } Section("Logs") { diff --git a/Meshtastic/Views/Nodes/Helpers/NodeInfoItem.swift b/Meshtastic/Views/Nodes/Helpers/NodeInfoItem.swift index 07f3d92c..eb4c37b0 100644 --- a/Meshtastic/Views/Nodes/Helpers/NodeInfoItem.swift +++ b/Meshtastic/Views/Nodes/Helpers/NodeInfoItem.swift @@ -31,6 +31,7 @@ struct NodeInfoItem: View { .foregroundStyle(.gray) .font(.callout) } + .accessibilityElement(children: .combine) Spacer() } VStack(alignment: .center) { @@ -49,9 +50,11 @@ struct NodeInfoItem: View { .cornerRadius(5) } } + .accessibilityElement(children: .combine) } Spacer() } + .accessibilityElement(children: .combine) .onAppear { Api().loadDeviceHardwareData { (hw) in for device in hw { @@ -79,6 +82,7 @@ struct NodeInfoItem: View { Text(String("incomplete".localized)) } } + .accessibilityElement(children: .combine) } } } diff --git a/Meshtastic/Views/Nodes/Helpers/NodeListItem.swift b/Meshtastic/Views/Nodes/Helpers/NodeListItem.swift index 2978ceab..62dd5fd0 100644 --- a/Meshtastic/Views/Nodes/Helpers/NodeListItem.swift +++ b/Meshtastic/Views/Nodes/Helpers/NodeListItem.swift @@ -7,9 +7,99 @@ import SwiftUI import CoreLocation +import Foundation struct NodeListItem: View { + // Accessibility: Synthesized description for VoiceOver + private var accessibilityDescription: String { + var desc = "" + if let shortName = node.user?.shortName { + // Format the shortName using the String extension method + desc = shortName.formatNodeNameForVoiceOver() + } else if let longName = node.user?.longName { + desc = longName + } else { + desc = "unknown node" + } + if connected { + desc += ", currently connected" + } + if node.favorite { + desc += ", favorite" + } + if node.lastHeard != nil { + let formatter = RelativeDateTimeFormatter() + formatter.unitsStyle = .full + let relative = formatter.localizedString(for: node.lastHeard!, relativeTo: Date()) + desc += ", last heard " + relative + } + if node.isOnline { + desc += ", online" + } else { + desc += ", offline" + } + let role = DeviceRoles(rawValue: Int(node.user?.role ?? 0)) + if let roleName = role?.name { + desc += ", role: \(roleName)" + } + if node.hopsAway > 0 { + desc += ", \(node.hopsAway) hops away" + } + if let battery = node.latestDeviceMetrics?.batteryLevel { + // Check for plugged in and charging states, same logic as in BatteryCompact and BatteryGauge + if battery > 100 { + desc += ", " + NSLocalizedString("device_plugged_in", comment: "VoiceOver value for plugged in device") + } else if battery == 100 { + desc += ", " + NSLocalizedString("device_charging", comment: "VoiceOver value for charging device") + } else { + desc += ", battery \(battery)%" + } + } + // Add distance and heading/bearing if available, but only for non-connected nodes + if !connected, let (lastPosition, myCoord) = locationData { + let nodeCoord = CLLocation(latitude: lastPosition.nodeCoordinate!.latitude, longitude: lastPosition.nodeCoordinate!.longitude) + let metersAway = nodeCoord.distance(from: myCoord) + + // Distance information + let distanceFormatter = LengthFormatter() + distanceFormatter.unitStyle = .medium + let formattedDistance = distanceFormatter.string(fromMeters: metersAway) + // For VoiceOver, prepend 'Distance' (localized) + desc += ", " + String(format: "%@: %@", NSLocalizedString("distance", comment: "Distance label for VoiceOver"), formattedDistance) + + // Add bearing/heading information for VoiceOver + let trueBearing = getBearingBetweenTwoPoints(point1: myCoord, point2: nodeCoord) + let heading = Measurement(value: trueBearing, unit: UnitAngle.degrees) + let formattedHeading = heading.formatted(.measurement(width: .narrow, numberFormatStyle: .number.precision(.fractionLength(0)))) + // Using a direct format without requiring a new localization key + desc += ", " + NSLocalizedString("heading", comment: "Heading label for VoiceOver") + " " + formattedHeading + } + // Add signal strength if available + if node.snr != 0 && !node.viaMqtt { + let signalStrength: BLESignalStrength + if node.snr < -10 { + signalStrength = .weak + } else if node.snr < 5 { + signalStrength = .normal + } else { + signalStrength = .strong + } + let signalString: String + switch signalStrength { + case .weak: + signalString = NSLocalizedString("ble.signal.strength.weak", comment: "VoiceOver value for weak BLE signal strength") + case .normal: + signalString = NSLocalizedString("ble.signal.strength.normal", comment: "VoiceOver value for normal BLE signal strength") + case .strong: + signalString = NSLocalizedString("ble.signal.strength.strong", comment: "VoiceOver value for strong BLE signal strength") + } + desc += ", " + signalString + } + return desc + } + + @ObservedObject var node: NodeInfoEntity var connected: Bool var connectedNode: Int64 @@ -167,7 +257,10 @@ struct NodeListItem: View { } .padding(.top, 4) .padding(.bottom, 4) - } + // Accessibility: Make the whole row a single element for VoiceOver + .accessibilityElement(children: .ignore) + .accessibilityLabel(accessibilityDescription) + } } struct DefaultIcon: View { diff --git a/Meshtastic/Views/Nodes/NodeList.swift b/Meshtastic/Views/Nodes/NodeList.swift index 1e823020..6b305148 100644 --- a/Meshtastic/Views/Nodes/NodeList.swift +++ b/Meshtastic/Views/Nodes/NodeList.swift @@ -243,6 +243,8 @@ struct NodeList: View { phoneOnly: true ) } + // Make sure the ZStack passes through accessibility to the ConnectedDevice component + .accessibilityElement(children: .contain) ) } content: { if let node = selectedNode { @@ -261,6 +263,7 @@ struct NodeList: View { } label: { Image(systemName: "rectangle") } + .accessibilityLabel("Hide sidebar") } ConnectedDevice( bluetoothOn: bleManager.isSwitchedOn, @@ -269,6 +272,8 @@ struct NodeList: View { phoneOnly: true ) } + // Make sure the ZStack passes through accessibility to the ConnectedDevice component + .accessibilityElement(children: .contain) ) } } else { From 670ad44a1d641c3f872ad67eb89cef3194c1168b Mon Sep 17 00:00:00 2001 From: Benjamin Faershtein <119711889+RCGV1@users.noreply.github.com> Date: Sun, 11 May 2025 18:56:41 -0700 Subject: [PATCH 018/213] fix comment Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- Meshtastic/Extensions/CoreData/MessageEntityExtension.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Meshtastic/Extensions/CoreData/MessageEntityExtension.swift b/Meshtastic/Extensions/CoreData/MessageEntityExtension.swift index 95071cca..0cfbd579 100644 --- a/Meshtastic/Extensions/CoreData/MessageEntityExtension.swift +++ b/Meshtastic/Extensions/CoreData/MessageEntityExtension.swift @@ -34,7 +34,7 @@ extension MessageEntity { func displayTimestamp(aboveMessage: MessageEntity?) -> Bool { if let aboveMessage = aboveMessage { - return aboveMessage.timestamp.addingTimeInterval(900) < timestamp // 15 seconds + return aboveMessage.timestamp.addingTimeInterval(900) < timestamp // 15 minutes } return false // First message will have no timestamp } From 020af152d16c4d48cf62e06adc00a2087c03c3e1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nikola=20Da=C5=A1i=C4=87?= Date: Mon, 12 May 2025 15:50:19 +0200 Subject: [PATCH 019/213] Update Serbian translations --- Localizable.xcstrings | 285 +++++++++++++++++++++++++++++++++++++++++- 1 file changed, 280 insertions(+), 5 deletions(-) diff --git a/Localizable.xcstrings b/Localizable.xcstrings index aa5cbd4c..49242ba8 100644 --- a/Localizable.xcstrings +++ b/Localizable.xcstrings @@ -2284,7 +2284,14 @@ } }, "Administration Enabled" : { - + "localizations" : { + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Администрација је активирана" + } + } + } }, "Advanced" : { "localizations" : { @@ -4944,6 +4951,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" : { + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Укључивањем ове функције, потврђујете и изричито пристајете на пренос географске локације вашег уређаја у реалном времену преко MQTT протокола без шифровања. Ови подаци о локацији могу се користити у сврхе као што су извештавање на живој мапи, праћење уређаја и сродне телеметријске функције." + } + }, "zh-Hant-TW" : { "stringUnit" : { "state" : "translated", @@ -6108,6 +6121,12 @@ "value" : "Utilizzo del canale %@%%" } }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Искоришћеност канала %@%%" + } + }, "zh-Hant-TW" : { "stringUnit" : { "state" : "translated", @@ -6734,6 +6753,12 @@ "value" : "Supporto alla community" } }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Подршка заједнице" + } + }, "zh-Hant-TW" : { "stringUnit" : { "state" : "translated", @@ -6862,6 +6887,12 @@ "value" : "Conferma" } }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Потврди" + } + }, "zh-Hant-TW" : { "stringUnit" : { "state" : "translated", @@ -6900,6 +6931,12 @@ }, "Connect to MQTT via Proxy" : { "localizations" : { + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Повежите се на MQTT преко проксија" + } + }, "zh-Hant-TW" : { "stringUnit" : { "state" : "translated", @@ -6916,6 +6953,12 @@ "value" : "Collegare alla nuova radio?" } }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Повезати се на нови радио уређај?" + } + }, "zh-Hant-TW" : { "stringUnit" : { "state" : "translated", @@ -7018,6 +7061,12 @@ "value" : "Radio connessa" } }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Радио повезан" + } + }, "zh-Hant-TW" : { "stringUnit" : { "state" : "translated", @@ -7092,6 +7141,12 @@ "value" : "La connessione a una nuova radio cancellerà tutti i dati delle app sul telefono." } }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Повезивање са новим радиом ће обрисати све податке апликације на телефону." + } + }, "zh-Hant-TW" : { "stringUnit" : { "state" : "translated", @@ -7194,6 +7249,12 @@ }, "Consent to Share Unencrypted Node Data via MQTT" : { "localizations" : { + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Пристанак на дељење нешифрованих података чвора путем MQTT-а" + } + }, "zh-Hant-TW" : { "stringUnit" : { "state" : "translated", @@ -7730,6 +7791,12 @@ "value" : "Attualmente mostra i moduli che potrebbero non essere supportati da questo nodo." } }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Тренутно приказује модуле који можда нису подржани од стране овог чвора." + } + }, "zh-Hant-TW" : { "stringUnit" : { "state" : "translated", @@ -10495,6 +10562,12 @@ "value" : "Abilita la trasmissione di pacchetti via UDP sulla rete locale." } }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Омогућите емитовање пакета путем UDP-а преко локалне мреже." + } + }, "zh-Hant-TW" : { "stringUnit" : { "state" : "translated", @@ -10539,6 +10612,12 @@ "value" : "Abilita questo dispositivo come server Store and Forward. Richiede un dispositivo ESP32 con PSRAM." } }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Омогућите овај уређај као сервер за складиштење и прослеђивање. Захтева ESP32 уређај са PSRAM-ом." + } + }, "zh-Hant-TW" : { "stringUnit" : { "state" : "translated", @@ -10721,6 +10800,12 @@ }, "Enables the store and forward module." : { "localizations" : { + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Омогућава модул за складиштење и прослеђивање." + } + }, "zh-Hant-TW" : { "stringUnit" : { "state" : "translated", @@ -13145,6 +13230,12 @@ "value" : "Supporto completo" } }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Пуна подршка" + } + }, "zh-Hant-TW" : { "stringUnit" : { "state" : "translated", @@ -14719,6 +14810,12 @@ }, "I have read and understand the above. I voluntarily consent to the unencrypted transmission of my node data via MQTT." : { "localizations" : { + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Прочитао/ла сам и разумем горе наведено. Добровољно пристајем на нешифровани пренос података мог чвора путем MQTT-а." + } + }, "zh-Hant-TW" : { "stringUnit" : { "state" : "translated", @@ -15065,6 +15162,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" : { + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Игнорише посматране поруке из страних мрежа попут Local Only, али иде и корак даље тако што такође игнорише поруке са чворова који се већ не налазе на листи познатих чворова." + } + }, "zh-Hant-TW" : { "stringUnit" : { "state" : "translated", @@ -15075,6 +15178,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" : { + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Игнорише посматране поруке из страних мрежа које су отворене или које не може дешифровати. Пребацује поруке само на примарним/секундарним каналима локалног чвора." + } + }, "zh-Hant-TW" : { "stringUnit" : { "state" : "translated", @@ -15607,6 +15716,12 @@ }, "Jump to present" : { "localizations" : { + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Скочи на најновије" + } + }, "zh-Hant-TW" : { "stringUnit" : { "state" : "translated", @@ -17374,7 +17489,14 @@ } }, "message" : { - + "localizations" : { + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "порука" + } + } + } }, "Message" : { "localizations" : { @@ -20710,6 +20832,12 @@ }, "Only permitted for SENSOR, TRACKER and TAK_TRACKER roles, this will inhibit all rebroadcasts, not unlike CLIENT_MUTE role." : { "localizations" : { + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Дозвољено само за улоге SENSOR, TRACKER и TAK_TRACKER, ово ће спречити сва поновна емитовања, слично улози CLIENT_MUTE." + } + }, "zh-Hant-TW" : { "stringUnit" : { "state" : "translated", @@ -20720,6 +20848,12 @@ }, "Only rebroadcasts packets from the core portnums: NodeInfo, Text, Position, Telemetry, and Routing." : { "localizations" : { + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Само поново емитује пакете из основних портова: Информације о чвору, Текст, Позиција, Телеметрија и Рутирање." + } + }, "zh-Hant-TW" : { "stringUnit" : { "state" : "translated", @@ -21701,7 +21835,14 @@ } }, "paxcounter.log" : { - + "localizations" : { + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "paxcounter.log" + } + } + } }, "Perform a factory reset on the node you are connected to" : { "localizations" : { @@ -21957,6 +22098,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" : { + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Молимо вас да имате у виду да, пошто извештај мапе није шифрован, ваши подаци могу бити трајно сачувани и приказани од стране трећих лица. Meshtastic не преузима одговорност за такво чување, приказивање или откривање ових података." + } + }, "zh-Hant-TW" : { "stringUnit" : { "state" : "translated", @@ -22921,6 +23068,12 @@ "value" : "Pressione" } }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Притисак" + } + }, "zh-Hant-TW" : { "stringUnit" : { "state" : "translated", @@ -23399,6 +23552,12 @@ "value" : "Radiazioni" } }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Зрачење" + } + }, "zh-Hant-TW" : { "stringUnit" : { "state" : "translated", @@ -23837,6 +23996,12 @@ }, "Rebroadcast any observed message, if it was on our private channel or from another mesh with the same lora params." : { "localizations" : { + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Поново емитује сваку посматрану поруку, ако је била на нашем приватном каналу или из друге мреже са истим лора параметрима." + } + }, "zh-Hant-TW" : { "stringUnit" : { "state" : "translated", @@ -24498,7 +24663,14 @@ } }, "Replying to a message" : { - + "localizations" : { + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Одговара на поруку" + } + } + } }, "Request Legacy Admin: %@" : { "localizations" : { @@ -25632,6 +25804,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" : { + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Исто понашање као ALL, али прескаче декодирање пакета и једноставно их поново емитује. Доступно само у улози Repeater. Подешавање овога на било којој другој улози резултираће понашањем ALL." + } + }, "zh-Hant-TW" : { "stringUnit" : { "state" : "translated", @@ -26419,7 +26597,14 @@ } }, "Select a node from the drop down to manage connected or remote devices." : { - + "localizations" : { + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Изаберите чвор из падајућег менија за управљање повезаним или удаљеним уређајима." + } + } + } }, "Select a Trace Route" : { "localizations" : { @@ -26659,6 +26844,12 @@ "value" : "Invia un heartbeat per pubblicizzare la presenza del server." } }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Пошаљите откуцај срца за оглашавање присуства сервера." + } + }, "zh-Hant-TW" : { "stringUnit" : { "state" : "translated", @@ -27786,6 +27977,12 @@ "value" : "Opzione server" } }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Опција сервера" + } + }, "zh-Hant-TW" : { "stringUnit" : { "state" : "translated", @@ -28516,6 +28713,12 @@ "value" : "Mostra le informazioni relative alla radio Lora collegata via bluetooth. È possibile scorrere il dito verso sinistra per scollegare la radio e premere a lungo per avviare l'attività live." } }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Приказује информације за Лора радио повезан путем блутута. Можете превући улево да прекинете везу са радиом, а дугим притиском почети активност уживо." + } + }, "zh-Hant-TW" : { "stringUnit" : { "state" : "translated", @@ -28984,6 +29187,12 @@ "value" : "Umidità del suolo" } }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Влажност земљишта" + } + }, "zh-Hant-TW" : { "stringUnit" : { "state" : "translated", @@ -29000,6 +29209,12 @@ "value" : "Temperatura del suolo" } }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Температура земљишта" + } + }, "zh-Hant-TW" : { "stringUnit" : { "state" : "translated", @@ -29476,6 +29691,12 @@ "value" : "I server Store and Forward richiedono un dispositivo ESP32 con PSRAM o Linux Native." } }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Сервери за складиштење и прослеђивање захтевају ESP32 уређај са PSRAM-ом или Linux Native." + } + }, "zh-Hant-TW" : { "stringUnit" : { "state" : "translated", @@ -30750,6 +30971,12 @@ "value" : "I ruoli di router sono progettati per posizioni elevate, come le cime delle montagne e le torri. Questo nodo deve essere in grado di avere una buona connessione diretta con la maggior parte dei nodi della rete, altrimenti danneggia significativamente la rete." } }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Улоге рутера су дизајниране за локације са добром прегледношћу попут планинских врхова и торњева. Овај чвор мора имати добру директну везу са већином чворова на мрежи, иначе ће значајно нарушити мрежу." + } + }, "zh-Hant-TW" : { "stringUnit" : { "state" : "translated", @@ -31240,6 +31467,12 @@ "value" : "Questo nodo non supporta alcun modulo configurabile." } }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Овај чвор не подржава ниједан конфигурабилни модул." + } + }, "zh-Hant-TW" : { "stringUnit" : { "state" : "translated", @@ -31810,6 +32043,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" : { + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "У складу са законима о приватности попут CCPA и GDPR, избегавамо дељење тачних података о локацији. Уместо тога, користимо анонимизоване или приближне (непрецизне) информације о локацији како бисмо заштитили вашу приватност." + } + }, "zh-Hant-TW" : { "stringUnit" : { "state" : "translated", @@ -32578,6 +32817,12 @@ "value" : "Trasmissione UDP" } }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "УДП емитовање" + } + }, "zh-Hant-TW" : { "stringUnit" : { "state" : "translated", @@ -33879,6 +34124,12 @@ "state" : "new", "value" : "Version: %1$@ (%2$@)" } + }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Верзија: %1$@ (%2$@)" + } } } }, @@ -34066,6 +34317,12 @@ "value" : "Volt %@" } }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Волти %@" + } + }, "zh-Hant-TW" : { "stringUnit" : { "state" : "translated", @@ -34456,6 +34713,12 @@ "value" : "Peso" } }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Тежина" + } + }, "zh-Hant-TW" : { "stringUnit" : { "state" : "translated", @@ -34760,6 +35023,12 @@ "value" : "Vento" } }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ветар" + } + }, "zh-Hant-TW" : { "stringUnit" : { "state" : "translated", @@ -35213,6 +35482,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" : { + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ваш чвор ће периодично слати нешифровани пакет извештаја мапе на конфигурисани MQTT сервер, што укључује ИД, кратко и дуго име, приближну локацију, модел хардвера, улогу, верзију фирмвера, ЛоРа регион, модем пресет и назив примарног канала." + } + }, "zh-Hant-TW" : { "stringUnit" : { "state" : "translated", From 69e7a8ce4c841c0f605b444bbd3665d6f5876ea1 Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Tue, 13 May 2025 06:19:27 -0700 Subject: [PATCH 020/213] Remove legacy admin --- Localizable.xcstrings | 1 + Meshtastic/Views/Settings/Config/BluetoothConfig.swift | 3 +-- Meshtastic/Views/Settings/Config/DeviceConfig.swift | 3 +-- Meshtastic/Views/Settings/Config/DisplayConfig.swift | 3 +-- Meshtastic/Views/Settings/Config/LoRaConfig.swift | 8 +++++--- .../Settings/Config/Module/AmbientLightingConfig.swift | 3 +-- .../Settings/Config/Module/CannedMessagesConfig.swift | 3 +-- .../Settings/Config/Module/DetectionSensorConfig.swift | 3 +-- .../Config/Module/ExternalNotificationConfig.swift | 3 +-- Meshtastic/Views/Settings/Config/Module/MQTTConfig.swift | 3 +-- .../Views/Settings/Config/Module/PaxCounterConfig.swift | 3 +-- .../Views/Settings/Config/Module/RangeTestConfig.swift | 3 +-- Meshtastic/Views/Settings/Config/Module/RtttlConfig.swift | 3 +-- .../Views/Settings/Config/Module/SerialConfig.swift | 3 +-- .../Views/Settings/Config/Module/StoreForwardConfig.swift | 3 +-- .../Views/Settings/Config/Module/TelemetryConfig.swift | 3 +-- Meshtastic/Views/Settings/Config/NetworkConfig.swift | 3 +-- Meshtastic/Views/Settings/Config/PositionConfig.swift | 3 +-- Meshtastic/Views/Settings/Config/PowerConfig.swift | 3 +-- Meshtastic/Views/Settings/Config/SecurityConfig.swift | 3 +-- 20 files changed, 24 insertions(+), 39 deletions(-) diff --git a/Localizable.xcstrings b/Localizable.xcstrings index c4c9d2ca..c62f4e6a 100644 --- a/Localizable.xcstrings +++ b/Localizable.xcstrings @@ -22468,6 +22468,7 @@ } }, "Positon config received: %@" : { + "extractionState" : "stale", "localizations" : { "de" : { "stringUnit" : { diff --git a/Meshtastic/Views/Settings/Config/BluetoothConfig.swift b/Meshtastic/Views/Settings/Config/BluetoothConfig.swift index 82b4f628..81b43499 100644 --- a/Meshtastic/Views/Settings/Config/BluetoothConfig.swift +++ b/Meshtastic/Views/Settings/Config/BluetoothConfig.swift @@ -115,8 +115,7 @@ struct BluetoothConfig: View { } } else { /// Legacy Administration - Logger.mesh.info("☠️ Using insecure legacy admin, empty bluetooth config") - _ = bleManager.requestBluetoothConfig(fromUser: connectedNode.user!, toUser: node.user!, adminIndex: connectedNode.myInfo?.adminIndex ?? 0) + Logger.mesh.info("☠️ Using insecure legacy admin that is no longer supported, please upgrade your firmware.") } } } diff --git a/Meshtastic/Views/Settings/Config/DeviceConfig.swift b/Meshtastic/Views/Settings/Config/DeviceConfig.swift index 6539848a..cd812e5d 100644 --- a/Meshtastic/Views/Settings/Config/DeviceConfig.swift +++ b/Meshtastic/Views/Settings/Config/DeviceConfig.swift @@ -273,8 +273,7 @@ struct DeviceConfig: View { } else { if node.deviceConfig == nil { /// Legacy Administration - Logger.mesh.info("☠️ Using insecure legacy admin, empty device config") - _ = bleManager.requestDeviceConfig(fromUser: connectedNode.user!, toUser: node.user!, adminIndex: connectedNode.myInfo?.adminIndex ?? 0) + Logger.mesh.info("☠️ Using insecure legacy admin that is no longer supported, please upgrade your firmware.") } } } diff --git a/Meshtastic/Views/Settings/Config/DisplayConfig.swift b/Meshtastic/Views/Settings/Config/DisplayConfig.swift index d6f14135..c9029408 100644 --- a/Meshtastic/Views/Settings/Config/DisplayConfig.swift +++ b/Meshtastic/Views/Settings/Config/DisplayConfig.swift @@ -178,8 +178,7 @@ struct DisplayConfig: View { } } else { /// Legacy Administration - Logger.mesh.info("☠️ Using insecure legacy admin, empty display config") - _ = bleManager.requestDisplayConfig(fromUser: connectedNode.user!, toUser: node.user!, adminIndex: connectedNode.myInfo?.adminIndex ?? 0) + Logger.mesh.info("☠️ Using insecure legacy admin that is no longer supported, please upgrade your firmware.") } } } diff --git a/Meshtastic/Views/Settings/Config/LoRaConfig.swift b/Meshtastic/Views/Settings/Config/LoRaConfig.swift index 196f6ffb..1aae6ea4 100644 --- a/Meshtastic/Views/Settings/Config/LoRaConfig.swift +++ b/Meshtastic/Views/Settings/Config/LoRaConfig.swift @@ -248,13 +248,15 @@ struct LoRaConfig: View { /// 2.5 Administration with session passkey let expiration = node.sessionExpiration ?? Date() if expiration < Date() || node.loRaConfig == nil { + Logger.mesh.info("⚙️ Empty or expired lora config requesting via PKI admin") - _ = bleManager.requestLoRaConfig(fromUser: connectedNode.user!, toUser: node.user!, adminIndex: connectedNode.myInfo?.adminIndex ?? 0) + if connectedNode.user != nil && node.user != nil { + _ = bleManager.requestLoRaConfig(fromUser: connectedNode.user!, toUser: node.user!, adminIndex: connectedNode.myInfo?.adminIndex ?? 0) + } } } else { /// Legacy Administration - Logger.mesh.info("☠️ Using insecure legacy admin, empty lora config") - _ = bleManager.requestLoRaConfig(fromUser: connectedNode.user!, toUser: node.user!, adminIndex: connectedNode.myInfo?.adminIndex ?? 0) + Logger.mesh.info("☠️ Using insecure legacy admin that is no longer supported, please upgrade your firmware.") } } } diff --git a/Meshtastic/Views/Settings/Config/Module/AmbientLightingConfig.swift b/Meshtastic/Views/Settings/Config/Module/AmbientLightingConfig.swift index 24c0ada3..efbeed70 100644 --- a/Meshtastic/Views/Settings/Config/Module/AmbientLightingConfig.swift +++ b/Meshtastic/Views/Settings/Config/Module/AmbientLightingConfig.swift @@ -100,8 +100,7 @@ struct AmbientLightingConfig: View { } } else { /// Legacy Administration - Logger.mesh.info("☠️ Using insecure legacy admin, empty ambient lighting module config") - _ = bleManager.requestAmbientLightingConfig(fromUser: connectedNode.user!, toUser: node.user!, adminIndex: connectedNode.myInfo?.adminIndex ?? 0) + Logger.mesh.info("☠️ Using insecure legacy admin that is no longer supported, please upgrade your firmware.") } } } diff --git a/Meshtastic/Views/Settings/Config/Module/CannedMessagesConfig.swift b/Meshtastic/Views/Settings/Config/Module/CannedMessagesConfig.swift index 20a62136..0fbcbcf8 100644 --- a/Meshtastic/Views/Settings/Config/Module/CannedMessagesConfig.swift +++ b/Meshtastic/Views/Settings/Config/Module/CannedMessagesConfig.swift @@ -248,8 +248,7 @@ struct CannedMessagesConfig: View { } } else { /// Legacy Administration - Logger.mesh.info("☠️ Using insecure legacy admin, empty canned messages module config") - _ = bleManager.requestCannedMessagesModuleConfig(fromUser: connectedNode.user!, toUser: node.user!, adminIndex: connectedNode.myInfo?.adminIndex ?? 0) + Logger.mesh.info("☠️ Using insecure legacy admin that is no longer supported, please upgrade your firmware.") } } } diff --git a/Meshtastic/Views/Settings/Config/Module/DetectionSensorConfig.swift b/Meshtastic/Views/Settings/Config/Module/DetectionSensorConfig.swift index e042ca77..94b96b5d 100644 --- a/Meshtastic/Views/Settings/Config/Module/DetectionSensorConfig.swift +++ b/Meshtastic/Views/Settings/Config/Module/DetectionSensorConfig.swift @@ -206,8 +206,7 @@ struct DetectionSensorConfig: View { } } else { /// Legacy Administration - Logger.mesh.info("☠️ Using insecure legacy admin, empty detection sensor module config") - _ = bleManager.requestDetectionSensorModuleConfig(fromUser: connectedNode.user!, toUser: node.user!, adminIndex: connectedNode.myInfo?.adminIndex ?? 0) + Logger.mesh.info("☠️ Using insecure legacy admin that is no longer supported, please upgrade your firmware.") } } } diff --git a/Meshtastic/Views/Settings/Config/Module/ExternalNotificationConfig.swift b/Meshtastic/Views/Settings/Config/Module/ExternalNotificationConfig.swift index e9959b41..08745f04 100644 --- a/Meshtastic/Views/Settings/Config/Module/ExternalNotificationConfig.swift +++ b/Meshtastic/Views/Settings/Config/Module/ExternalNotificationConfig.swift @@ -214,8 +214,7 @@ struct ExternalNotificationConfig: View { } } else { /// Legacy Administration - Logger.mesh.info("☠️ Using insecure legacy admin, empty external notificaiton module config") - _ = bleManager.requestExternalNotificationModuleConfig(fromUser: connectedNode.user!, toUser: node.user!, adminIndex: connectedNode.myInfo?.adminIndex ?? 0) + Logger.mesh.info("☠️ Using insecure legacy admin that is no longer supported, please upgrade your firmware.") } } } diff --git a/Meshtastic/Views/Settings/Config/Module/MQTTConfig.swift b/Meshtastic/Views/Settings/Config/Module/MQTTConfig.swift index 6967a2ab..bd2dfeeb 100644 --- a/Meshtastic/Views/Settings/Config/Module/MQTTConfig.swift +++ b/Meshtastic/Views/Settings/Config/Module/MQTTConfig.swift @@ -365,8 +365,7 @@ struct MQTTConfig: View { } } else { /// Legacy Administration - Logger.mesh.info("☠️ Using insecure legacy admin, empty mqtt module config") - _ = bleManager.requestMqttModuleConfig(fromUser: connectedNode.user!, toUser: node.user!, adminIndex: connectedNode.myInfo?.adminIndex ?? 0) + Logger.mesh.info("☠️ Using insecure legacy admin that is no longer supported, please upgrade your firmware.") } } } diff --git a/Meshtastic/Views/Settings/Config/Module/PaxCounterConfig.swift b/Meshtastic/Views/Settings/Config/Module/PaxCounterConfig.swift index 6d71f7a7..7c84b406 100644 --- a/Meshtastic/Views/Settings/Config/Module/PaxCounterConfig.swift +++ b/Meshtastic/Views/Settings/Config/Module/PaxCounterConfig.swift @@ -73,8 +73,7 @@ struct PaxCounterConfig: View { } } else { /// Legacy Administration - Logger.mesh.info("☠️ Using insecure legacy admin, empty pax counter module config") - _ = bleManager.requestPaxCounterModuleConfig(fromUser: connectedNode.user!, toUser: node.user!, adminIndex: connectedNode.myInfo?.adminIndex ?? 0) + Logger.mesh.info("☠️ Using insecure legacy admin that is no longer supported, please upgrade your firmware.") } } } diff --git a/Meshtastic/Views/Settings/Config/Module/RangeTestConfig.swift b/Meshtastic/Views/Settings/Config/Module/RangeTestConfig.swift index cc323170..b2636967 100644 --- a/Meshtastic/Views/Settings/Config/Module/RangeTestConfig.swift +++ b/Meshtastic/Views/Settings/Config/Module/RangeTestConfig.swift @@ -96,8 +96,7 @@ struct RangeTestConfig: View { } } else { /// Legacy Administration - Logger.mesh.info("☠️ Using insecure legacy admin, empty range test module config") - _ = bleManager.requestRangeTestModuleConfig(fromUser: connectedNode.user!, toUser: node.user!, adminIndex: connectedNode.myInfo?.adminIndex ?? 0) + Logger.mesh.info("☠️ Using insecure legacy admin that is no longer supported, please upgrade your firmware.") } } } diff --git a/Meshtastic/Views/Settings/Config/Module/RtttlConfig.swift b/Meshtastic/Views/Settings/Config/Module/RtttlConfig.swift index 71452615..da30e1e4 100644 --- a/Meshtastic/Views/Settings/Config/Module/RtttlConfig.swift +++ b/Meshtastic/Views/Settings/Config/Module/RtttlConfig.swift @@ -87,8 +87,7 @@ struct RtttlConfig: View { } } else { /// Legacy Administration - Logger.mesh.info("☠️ Using insecure legacy admin, empty ringtone module config") - _ = bleManager.requestRtttlConfig(fromUser: connectedNode.user!, toUser: node.user!, adminIndex: connectedNode.myInfo?.adminIndex ?? 0) + Logger.mesh.info("☠️ Using insecure legacy admin that is no longer supported, please upgrade your firmware.") } } } diff --git a/Meshtastic/Views/Settings/Config/Module/SerialConfig.swift b/Meshtastic/Views/Settings/Config/Module/SerialConfig.swift index 0bbe9fbc..d1aca540 100644 --- a/Meshtastic/Views/Settings/Config/Module/SerialConfig.swift +++ b/Meshtastic/Views/Settings/Config/Module/SerialConfig.swift @@ -151,8 +151,7 @@ struct SerialConfig: View { } } else { /// Legacy Administration - Logger.mesh.info("☠️ Using insecure legacy admin, empty serial module config") - _ = bleManager.requestSerialModuleConfig(fromUser: connectedNode.user!, toUser: node.user!, adminIndex: connectedNode.myInfo?.adminIndex ?? 0) + Logger.mesh.info("☠️ Using insecure legacy admin that is no longer supported, please upgrade your firmware.") } } } diff --git a/Meshtastic/Views/Settings/Config/Module/StoreForwardConfig.swift b/Meshtastic/Views/Settings/Config/Module/StoreForwardConfig.swift index e9fc429f..c49fadf1 100644 --- a/Meshtastic/Views/Settings/Config/Module/StoreForwardConfig.swift +++ b/Meshtastic/Views/Settings/Config/Module/StoreForwardConfig.swift @@ -152,8 +152,7 @@ struct StoreForwardConfig: View { } } else { /// Legacy Administration - Logger.mesh.info("☠️ Using insecure legacy admin, empty store & forward module config") - _ = bleManager.requestStoreAndForwardModuleConfig(fromUser: connectedNode.user!, toUser: node.user!, adminIndex: connectedNode.myInfo?.adminIndex ?? 0) + Logger.mesh.info("☠️ Using insecure legacy admin that is no longer supported, please upgrade your firmware.") } } } diff --git a/Meshtastic/Views/Settings/Config/Module/TelemetryConfig.swift b/Meshtastic/Views/Settings/Config/Module/TelemetryConfig.swift index 0dfe7566..0ee48f86 100644 --- a/Meshtastic/Views/Settings/Config/Module/TelemetryConfig.swift +++ b/Meshtastic/Views/Settings/Config/Module/TelemetryConfig.swift @@ -149,8 +149,7 @@ struct TelemetryConfig: View { } } else { /// Legacy Administration - Logger.mesh.info("☠️ Using insecure legacy admin, empty telemetry module config") - _ = bleManager.requestTelemetryModuleConfig(fromUser: connectedNode.user!, toUser: node.user!, adminIndex: connectedNode.myInfo?.adminIndex ?? 0) + Logger.mesh.info("☠️ Using insecure legacy admin that is no longer supported, please upgrade your firmware.") } } } diff --git a/Meshtastic/Views/Settings/Config/NetworkConfig.swift b/Meshtastic/Views/Settings/Config/NetworkConfig.swift index 35e25660..c4dcb8f2 100644 --- a/Meshtastic/Views/Settings/Config/NetworkConfig.swift +++ b/Meshtastic/Views/Settings/Config/NetworkConfig.swift @@ -158,8 +158,7 @@ struct NetworkConfig: View { } } else { /// Legacy Administration - Logger.mesh.info("☠️ Using insecure legacy admin, empty network config") - _ = bleManager.requestNetworkConfig(fromUser: connectedNode.user!, toUser: node.user!, adminIndex: connectedNode.myInfo?.adminIndex ?? 0) + Logger.mesh.info("☠️ Using insecure legacy admin that is no longer supported, please upgrade your firmware.") } } } diff --git a/Meshtastic/Views/Settings/Config/PositionConfig.swift b/Meshtastic/Views/Settings/Config/PositionConfig.swift index 8fbd9d14..3af2546d 100644 --- a/Meshtastic/Views/Settings/Config/PositionConfig.swift +++ b/Meshtastic/Views/Settings/Config/PositionConfig.swift @@ -416,8 +416,7 @@ struct PositionConfig: View { } } else { /// Legacy Administration - Logger.mesh.info("☠️ Using insecure legacy admin, empty position config") - _ = bleManager.requestPositionConfig(fromUser: connectedNode.user!, toUser: node.user!, adminIndex: connectedNode.myInfo?.adminIndex ?? 0) + Logger.mesh.info("☠️ Using insecure legacy admin that is no longer supported, please upgrade your firmware.") } } } diff --git a/Meshtastic/Views/Settings/Config/PowerConfig.swift b/Meshtastic/Views/Settings/Config/PowerConfig.swift index e3c26ffb..9ba385ff 100644 --- a/Meshtastic/Views/Settings/Config/PowerConfig.swift +++ b/Meshtastic/Views/Settings/Config/PowerConfig.swift @@ -143,8 +143,7 @@ struct PowerConfig: View { } } else { /// Legacy Administration - Logger.mesh.info("☠️ Using insecure legacy admin, empty power config") - _ = bleManager.requestPowerConfig(fromUser: connectedNode.user!, toUser: node.user!, adminIndex: connectedNode.myInfo?.adminIndex ?? 0) + Logger.mesh.info("☠️ Using insecure legacy admin that is no longer supported, please upgrade your firmware.") } } } diff --git a/Meshtastic/Views/Settings/Config/SecurityConfig.swift b/Meshtastic/Views/Settings/Config/SecurityConfig.swift index 938a9202..13f40f98 100644 --- a/Meshtastic/Views/Settings/Config/SecurityConfig.swift +++ b/Meshtastic/Views/Settings/Config/SecurityConfig.swift @@ -204,8 +204,7 @@ struct SecurityConfig: View { } else { if node.deviceConfig == nil { /// Legacy Administration - Logger.mesh.info("☠️ Using insecure legacy admin, empty security config") - _ = bleManager.requestSecurityConfig(fromUser: connectedNode.user!, toUser: node.user!, adminIndex: connectedNode.myInfo?.adminIndex ?? 0) + Logger.mesh.info("☠️ Using insecure legacy admin that is no longer supported, please upgrade your firmware.") } } } From 6b001471d555aa04736831dfd85929c256c1e2e7 Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Tue, 13 May 2025 06:29:39 -0700 Subject: [PATCH 021/213] Revert "Additional accessibilityLabels for VoiceOver users (take #2)" --- Localizable.xcstrings | 480 +----------------- Meshtastic/Extensions/String.swift | 11 - Meshtastic/Views/Bluetooth/Connect.swift | 29 -- .../Helpers/BLESignalStrengthIndicator.swift | 82 ++- Meshtastic/Views/Helpers/BatteryCompact.swift | 89 +--- Meshtastic/Views/Helpers/BatteryGauge.swift | 35 +- .../Views/Helpers/ConnectedDevice.swift | 50 +- .../RequestPositionButton.swift | 1 - .../TextMessageField/TextMessageSize.swift | 2 - .../Helpers/Actions/IgnoreNodeButton.swift | 1 - .../Views/Nodes/Helpers/NodeDetail.swift | 165 +++--- .../Views/Nodes/Helpers/NodeInfoItem.swift | 4 - .../Views/Nodes/Helpers/NodeListItem.swift | 95 +--- Meshtastic/Views/Nodes/NodeList.swift | 5 - 14 files changed, 154 insertions(+), 895 deletions(-) diff --git a/Localizable.xcstrings b/Localizable.xcstrings index 16941a31..c62f4e6a 100644 --- a/Localizable.xcstrings +++ b/Localizable.xcstrings @@ -35346,485 +35346,7 @@ } } } - }, - "ble.signal.strength.weak" : { - "comment" : "VoiceOver value for weak BLE signal strength", - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "needs_review", - "value" : "Signalstärke schwach" - } - }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Signal strength weak" - } - }, - "it" : { - "stringUnit" : { - "state" : "needs_review", - "value" : "Segnale debole" - } - }, - "sr" : { - "stringUnit" : { - "state" : "needs_review", - "value" : "Слаб сигнал" - } - }, - "zh-Hans" : { - "stringUnit" : { - "state" : "needs_review", - "value" : "信号弱" - } - }, - "zh-Hant-TW" : { - "stringUnit" : { - "state" : "needs_review", - "value" : "訊號微弱" - } - } - } - }, - "signal_strength" : { - "comment" : "VoiceOver label for signal strength indicator", - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "needs_review", - "value" : "Signalstärke" - } - }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Signal strength" - } - }, - "it" : { - "stringUnit" : { - "state" : "needs_review", - "value" : "Intensità del segnale" - } - }, - "sr" : { - "stringUnit" : { - "state" : "needs_review", - "value" : "Јачина сигнала" - } - }, - "zh-Hans" : { - "stringUnit" : { - "state" : "needs_review", - "value" : "信号强度" - } - }, - "zh-Hant-TW" : { - "stringUnit" : { - "state" : "needs_review", - "value" : "訊號強度" - } - } - } - }, - "message_size" : { - "comment" : "VoiceOver label for message size", - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "needs_review", - "value" : "Nachrichtengröße" - } - }, - "en" : { - "stringUnit" : { - "state" : "needs_review", - "value" : "Message size" - } - }, - "it" : { - "stringUnit" : { - "state" : "needs_review", - "value" : "Dimensione messaggio" - } - }, - "sr" : { - "stringUnit" : { - "state" : "needs_review", - "value" : "Величина поруке" - } - }, - "zh-Hans" : { - "stringUnit" : { - "state" : "needs_review", - "value" : "消息大小" - } - }, - "zh-Hant-TW" : { - "stringUnit" : { - "state" : "needs_review", - "value" : "訊息大小" - } - } - } - }, - "device_charging" : { - "comment" : "VoiceOver value for charging device", - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Charging" - } - } - } - }, - "Bluetooth is off.off" : { - "extractionState" : "stale", - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Bluetooth ist aus" - } - }, - "fr" : { - "stringUnit" : { - "state" : "translated", - "value" : "Le Bluetooth est arrêté" - } - }, - "he" : { - "stringUnit" : { - "state" : "translated", - "value" : "בלוטוס כבוי" - } - }, - "it" : { - "stringUnit" : { - "state" : "translated", - "value" : "Il Bluetooth è spento" - } - }, - "pl" : { - "stringUnit" : { - "state" : "translated", - "value" : "Bluetooth jest wyłączony" - } - }, - "se" : { - "stringUnit" : { - "state" : "translated", - "value" : "Bluetooth är avstängt" - } - }, - "sr" : { - "stringUnit" : { - "state" : "translated", - "value" : "Блутут је искључен" - } - }, - "zh-Hans" : { - "stringUnit" : { - "state" : "translated", - "value" : "蓝牙已关闭" - } - }, - "zh-Hant-TW" : { - "stringUnit" : { - "state" : "translated", - "value" : "藍芽已關閉" - } - } - } - }, - "bytes_used" : { - "comment" : "VoiceOver value for bytes used", - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "needs_review", - "value" : "%d von %d Bytes verwendet" - } - }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "%d of %d bytes used" - } - }, - "it" : { - "stringUnit" : { - "state" : "needs_review", - "value" : "%d di %d byte usati" - } - }, - "sr" : { - "stringUnit" : { - "state" : "needs_review", - "value" : "%d од %d бајтова искоришћено" - } - }, - "zh-Hans" : { - "stringUnit" : { - "state" : "needs_review", - "value" : "已用%d/%d字节" - } - }, - "zh-Hant-TW" : { - "stringUnit" : { - "state" : "needs_review", - "value" : "已用%d/%d位元組" - } - } - } - }, - "heading" : { - "comment" : "Heading label for VoiceOver" - }, - "Hide sidebar" : {}, - "bluetooth.not.connected" : { - "comment" : "VoiceOver label for disconnected Bluetooth icon", - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "No Bluetooth device connected" - } - } - } - }, - "device.configuration" : { - "extractionState" : "stale", - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "needs_review", - "value" : "Gerätekonfiguration" - } - }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Device Configuration" - } - }, - "he" : { - "stringUnit" : { - "state" : "needs_review", - "value" : "Device Configuration" - } - }, - "it" : { - "stringUnit" : { - "state" : "needs_review", - "value" : "Configurazione del dispositivo" - } - }, - "pl" : { - "stringUnit" : { - "state" : "needs_review", - "value" : "Device Configuration" - } - }, - "se" : { - "stringUnit" : { - "state" : "needs_review", - "value" : "Enhetsinställningar" - } - }, - "sr" : { - "stringUnit" : { - "state" : "needs_review", - "value" : "Подешавања уређаја" - } - }, - "zh-Hans" : { - "stringUnit" : { - "state" : "needs_review", - "value" : "设备配置" - } - }, - "zh-Hant-TW" : { - "stringUnit" : { - "state" : "needs_review", - "value" : "設備設定" - } - } - } - }, - "ble.signal.strength.strong" : { - "comment" : "VoiceOver value for strong BLE signal strength", - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "needs_review", - "value" : "Signalstärke stark" - } - }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Signal strength strong" - } - }, - "it" : { - "stringUnit" : { - "state" : "needs_review", - "value" : "Segnale forte" - } - }, - "sr" : { - "stringUnit" : { - "state" : "needs_review", - "value" : "Јак сигнал" - } - }, - "zh-Hans" : { - "stringUnit" : { - "state" : "needs_review", - "value" : "信号强" - } - }, - "zh-Hant-TW" : { - "stringUnit" : { - "state" : "needs_review", - "value" : "訊號強" - } - } - } - }, - "ble.signal.strength.normal" : { - "comment" : "VoiceOver value for normal BLE signal strength", - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "needs_review", - "value" : "Signalstärke normal" - } - }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Signal strength normal" - } - }, - "it" : { - "stringUnit" : { - "state" : "needs_review", - "value" : "Segnale normale" - } - }, - "sr" : { - "stringUnit" : { - "state" : "needs_review", - "value" : "Нормалан сигнал" - } - }, - "zh-Hans" : { - "stringUnit" : { - "state" : "needs_review", - "value" : "信号正常" - } - }, - "zh-Hant-TW" : { - "stringUnit" : { - "state" : "needs_review", - "value" : "訊號正常" - } - } - } - }, - "bluetooth.connected" : { - "comment" : "VoiceOver label for connected Bluetooth icon", - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Connected to Bluetooth device" - } - } - } - }, - "request_position" : { - "comment" : "VoiceOver label for request position button", - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Position anfordern" - } - }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Request position" - } - }, - "it" : { - "stringUnit" : { - "state" : "translated", - "value" : "Richiedi posizione" - } - }, - "sr" : { - "stringUnit" : { - "state" : "translated", - "value" : "Захтевај позицију" - } - }, - "zh-Hans" : { - "stringUnit" : { - "state" : "translated", - "value" : "请求位置" - } - }, - "zh-Hant-TW" : { - "stringUnit" : { - "state" : "translated", - "value" : "請求位置" - } - } - } - }, - "distance" : { - "comment" : "Distance label for VoiceOver" - }, - "device_plugged_in" : { - "comment" : "VoiceOver value for plugged in device", - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Plugged in" - } - } - } - }, - "unknown" : { - "localizations" : { - "it" : { - "stringUnit" : { - "state" : "needs_review", - "value" : "sconosciuto" - } - }, - "sr" : { - "stringUnit" : { - "state" : "needs_review", - "value" : "непознато" - } - }, - "zh-Hant-TW" : { - "stringUnit" : { - "state" : "needs_review", - "value" : "未知" - } - } - } } }, "version" : "1.0" -} +} \ No newline at end of file diff --git a/Meshtastic/Extensions/String.swift b/Meshtastic/Extensions/String.swift index 6a57da9e..d2ae1e5a 100644 --- a/Meshtastic/Extensions/String.swift +++ b/Meshtastic/Extensions/String.swift @@ -115,17 +115,6 @@ extension String { .joined() } - /// Formats a short name like "P130" to read as "Node P 130" for VoiceOver - /// This ensures proper pronunciation of alphanumeric node IDs - func formatNodeNameForVoiceOver() -> String { - let spaced = self.replacingOccurrences( - of: #"([A-Za-z])([0-9]+)"#, - with: "$1 $2", - options: .regularExpression - ) - return "Node " + spaced - } - // Adds variation selectors to prefer the graphical form of emoji. // Looks ahead to make sure that the variation selector is not already applied. var addingVariationSelectors: String { diff --git a/Meshtastic/Views/Bluetooth/Connect.swift b/Meshtastic/Views/Bluetooth/Connect.swift index 60bb2072..5e9dd834 100644 --- a/Meshtastic/Views/Bluetooth/Connect.swift +++ b/Meshtastic/Views/Bluetooth/Connect.swift @@ -27,31 +27,6 @@ struct Connect: View { @State var presentingSwitchPreferredPeripheral = false @State var selectedPeripherialId = "" - private func nodeAccessibilityLabel() -> String { - // Create a battery status string that handles charging and plugged in states - var batteryStatus: String? = nil - if let batteryLevel = node?.latestDeviceMetrics?.batteryLevel { - if batteryLevel > 100 { - // Plugged in state - batteryStatus = NSLocalizedString("device_plugged_in", comment: "VoiceOver value for plugged in device") - } else if batteryLevel == 100 { - // Charging state - batteryStatus = NSLocalizedString("device_charging", comment: "VoiceOver value for charging device") - } else { - // Normal battery percentage - batteryStatus = "Battery: \(Int(batteryLevel))%" - } - } - - return [ - node?.user?.shortName?.formatNodeNameForVoiceOver() ?? "", - "BLE Name: \(bleManager.connectedPeripheral?.peripheral.name?.addingVariationSelectors ?? "unknown".localized)", - "Firmware Version: \(node?.metadata?.firmwareVersion ?? "unknown".localized)", - bleManager.isSubscribed ? "Subscribed" : nil, - batteryStatus - ].compactMap { $0 }.joined(separator: ", ") - } - init () { let notificationCenter = UNUserNotificationCenter.current() notificationCenter.getNotificationSettings(completionHandler: { (settings) in @@ -111,8 +86,6 @@ struct Connect: View { } } } - .accessibilityElement(children: .ignore) - .accessibilityLabel(nodeAccessibilityLabel()) .font(.caption) .foregroundColor(Color.gray) .padding([.top]) @@ -326,8 +299,6 @@ struct Connect: View { mqttTopic: bleManager.mqttManager.topic ) } - // Make sure the ZStack passes through accessibility to the ConnectedDevice component - .accessibilityElement(children: .contain) ) } .sheet(isPresented: $invalidFirmwareVersion, onDismiss: didDismissSheet) { diff --git a/Meshtastic/Views/Helpers/BLESignalStrengthIndicator.swift b/Meshtastic/Views/Helpers/BLESignalStrengthIndicator.swift index 73d38f98..c5d17f16 100644 --- a/Meshtastic/Views/Helpers/BLESignalStrengthIndicator.swift +++ b/Meshtastic/Views/Helpers/BLESignalStrengthIndicator.swift @@ -32,65 +32,47 @@ import Foundation import SwiftUI struct SignalStrengthIndicator: View { - // Accessibility: VoiceOver description - private var accessibilityDescription: String { - switch signalStrength { - case .weak: - return NSLocalizedString("ble.signal.strength.weak", comment: "VoiceOver value for weak BLE signal strength") - case .normal: - return NSLocalizedString("ble.signal.strength.normal", comment: "VoiceOver value for normal BLE signal strength") - case .strong: - return NSLocalizedString("ble.signal.strength.strong", comment: "VoiceOver value for strong BLE signal strength") - } - } + let signalStrength: BLESignalStrength - let signalStrength: BLESignalStrength + var body: some View { + HStack { + ForEach(0..<3) { bar in + RoundedRectangle(cornerRadius: 3) + .divided(amount: (CGFloat(bar) + 1) / CGFloat(3)) + .fill(getColor().opacity(bar <= signalStrength.rawValue ? 1 : 0.3)) + .frame(width: 8, height: 40) + } + } + } - var body: some View { - Group { - HStack { - ForEach(0..<3) { bar in - RoundedRectangle(cornerRadius: 3) - .divided(amount: (CGFloat(bar) + 1) / CGFloat(3)) - .fill(getColor().opacity(bar <= signalStrength.rawValue ? 1 : 0.3)) - .frame(width: 8, height: 40) - accessibilityHidden(true) // Ensures bars are ignored - } - } - } - .accessibilityElement(children: .ignore) - .accessibilityLabel(NSLocalizedString("signal_strength", comment: "VoiceOver label for signal strength indicator")) - .accessibilityValue(accessibilityDescription) - } - - private func getColor() -> Color { - switch signalStrength { - case .weak: - return Color.red - case .normal: - return Color.yellow - case .strong: - return Color.green - } - } + private func getColor() -> Color { + switch signalStrength { + case .weak: + return Color.red + case .normal: + return Color.yellow + case .strong: + return Color.green + } + } } struct Divided: Shape { - var amount: CGFloat // Should be in range 0...1 - var shape: S - func path(in rect: CGRect) -> Path { - shape.path(in: rect.divided(atDistance: amount * rect.height, from: .maxYEdge).slice) - } + var amount: CGFloat // Should be in range 0...1 + var shape: S + func path(in rect: CGRect) -> Path { + shape.path(in: rect.divided(atDistance: amount * rect.height, from: .maxYEdge).slice) + } } extension Shape { - func divided(amount: CGFloat) -> Divided { - return Divided(amount: amount, shape: self) - } + func divided(amount: CGFloat) -> Divided { + return Divided(amount: amount, shape: self) + } } enum BLESignalStrength: Int { - case weak = 0 - case normal = 1 - case strong = 2 + case weak = 0 + case normal = 1 + case strong = 2 } diff --git a/Meshtastic/Views/Helpers/BatteryCompact.swift b/Meshtastic/Views/Helpers/BatteryCompact.swift index bb9819a2..4ac61d0c 100644 --- a/Meshtastic/Views/Helpers/BatteryCompact.swift +++ b/Meshtastic/Views/Helpers/BatteryCompact.swift @@ -13,104 +13,69 @@ struct BatteryCompact: View { var color: Color var body: some View { - // Group the battery icon and label in a single accessible container HStack(alignment: .center, spacing: 0) { if let batteryLevel { - // Check for plugged in state - let isPluggedIn = batteryLevel > 100 - let isCharging = batteryLevel == 100 - - // Battery icon selection based on level - if isPluggedIn { - Image(systemName: "powerplug") - .font(iconFont) - .foregroundColor(color) - .symbolRenderingMode(.multicolor) - .accessibilityHidden(true) // Hide from VoiceOver since container will handle it - } else if isCharging { + if batteryLevel == 100 { Image(systemName: "battery.100.bolt") .font(iconFont) .foregroundColor(color) .symbolRenderingMode(.multicolor) - .accessibilityHidden(true) - } else if batteryLevel > 74 { + } else if batteryLevel < 100 && batteryLevel > 74 { Image(systemName: "battery.75") .font(iconFont) .foregroundColor(color) .symbolRenderingMode(.multicolor) - .accessibilityHidden(true) - } else if batteryLevel > 49 { + } else if batteryLevel < 75 && batteryLevel > 49 { Image(systemName: "battery.50") .font(iconFont) .foregroundColor(color) .symbolRenderingMode(.multicolor) - .accessibilityHidden(true) - } else if batteryLevel > 14 { + } else if batteryLevel < 50 && batteryLevel > 14 { Image(systemName: "battery.25") .font(iconFont) .foregroundColor(color) .symbolRenderingMode(.multicolor) - .accessibilityHidden(true) - } else if batteryLevel > 0 { + } else if batteryLevel < 15 && batteryLevel > 0 { Image(systemName: "battery.0") .font(iconFont) .foregroundColor(color) .symbolRenderingMode(.multicolor) - .accessibilityHidden(true) - } else { + } else if batteryLevel == 0 { Image(systemName: "battery.0") .font(iconFont) .foregroundColor(.red) .symbolRenderingMode(.multicolor) - .accessibilityHidden(true) - } - - // Battery text label - if isPluggedIn { - Text("PWD") - .foregroundStyle(.secondary) - .font(font) - .accessibilityHidden(true) - } else if isCharging { - Text("CHG") - .foregroundStyle(.secondary) - .font(font) - .accessibilityHidden(true) - } else { - Text(verbatim: "\(batteryLevel.formatted(.number.precision(.fractionLength(0))))%") - .foregroundStyle(.secondary) - .font(font) - .accessibilityHidden(true) + } else if batteryLevel > 100 { + Image(systemName: "powerplug") + .font(iconFont) + .foregroundColor(color) + .symbolRenderingMode(.multicolor) } } else { - // Unknown battery state Image(systemName: "battery.0") .font(iconFont) .foregroundColor(color) .symbolRenderingMode(.multicolor) - .accessibilityHidden(true) - + } + if let batteryLevel { + if batteryLevel > 100 { + Text("PWD") + .foregroundStyle(.secondary) + .font(font) + } else if batteryLevel == 100 { + Text("CHG") + .foregroundStyle(.secondary) + .font(font) + } else { + Text(verbatim: "\(batteryLevel.formatted(.number.precision(.fractionLength(0))))%") + .foregroundStyle(.secondary) + .font(font) + } + } else { Text(verbatim: "?") .foregroundStyle(.secondary) .font(font) - .accessibilityHidden(true) } } - // Setup container-level accessibility for VoiceOver - .accessibilityElement(children: .ignore) - .accessibilityLabel(NSLocalizedString("Battery Level", comment: "VoiceOver label for battery gauge")) - // Set appropriate value based on the battery state using a computed property - .accessibilityValue(batteryLevel.map { level in - if level > 100 { - // Plugged in - same as PWD visual indicator - return NSLocalizedString("device_plugged_in", comment: "VoiceOver value for plugged in device") - } else if level == 100 { - // Charging - same as CHG visual indicator - return NSLocalizedString("device_charging", comment: "VoiceOver value for charging device") - } else { - // Normal battery level - return String(format: NSLocalizedString("battery_level_percent", comment: "VoiceOver value for battery level"), Int(level)) - } - } ?? "Unknown") } } diff --git a/Meshtastic/Views/Helpers/BatteryGauge.swift b/Meshtastic/Views/Helpers/BatteryGauge.swift index 81e81e7e..952c9768 100644 --- a/Meshtastic/Views/Helpers/BatteryGauge.swift +++ b/Meshtastic/Views/Helpers/BatteryGauge.swift @@ -18,20 +18,18 @@ struct BatteryGauge: View { let deviceMetrics = node.telemetries?.filtered(using: NSPredicate(format: "metricsType == 0")) let mostRecent = deviceMetrics?.lastObject as? TelemetryEntity - // For VoiceOver purposes, detect when device is plugged in (battery > 100%) - let isPluggedIn = (mostRecent?.batteryLevel ?? 0) > 100 - // Use a capped battery level for UI display - let batteryLevel = Double(min(100, mostRecent?.batteryLevel ?? 0)) + let batteryLevel = Double(mostRecent?.batteryLevel ?? 0) VStack { - if isPluggedIn { - // Use a completely standalone view for the plugged in state - // to avoid any VoiceOver confusion - PluggedInIndicator() + if batteryLevel > 100.0 { + // Plugged in + Image(systemName: "powerplug") + .font(.largeTitle) + .foregroundColor(.accentColor) + .symbolRenderingMode(.hierarchical) } else { let gradient = Gradient(colors: [.red, .orange, .green]) Gauge(value: batteryLevel, in: minValue...maxValue) { - // Accessibility for battery gauge if batteryLevel >= 0.0 && batteryLevel < 10 { Label("Battery Level %", systemImage: "battery.0") } else if batteryLevel >= 10.0 && batteryLevel < 25.00 { @@ -52,8 +50,6 @@ struct BatteryGauge: View { Text(Int(batteryLevel), format: .percent) } } - .accessibilityLabel(NSLocalizedString("Battery Level", comment: "VoiceOver label for battery gauge")) - .accessibilityValue(String(format: NSLocalizedString("Battery Level %", comment: "VoiceOver value for battery level"), Int(batteryLevel))) .tint(gradient) .gaugeStyle(.accessoryCircular) } @@ -67,23 +63,6 @@ struct BatteryGauge: View { } } -/// A dedicated view for showing a device is plugged in -/// With proper VoiceOver support that matches the visual indication -struct PluggedInIndicator: View { - var body: some View { - // This view is isolated from any battery measurement - // to ensure VoiceOver doesn't pick up any percentages - Image(systemName: "powerplug") - .font(.largeTitle) - .foregroundColor(.accentColor) - .symbolRenderingMode(.hierarchical) - // Override the accessibility to ensure correct VoiceOver announcement - .accessibilityElement(children: .ignore) - .accessibilityLabel(NSLocalizedString("Battery Level", comment: "VoiceOver label for battery gauge")) - .accessibilityValue(NSLocalizedString("device_plugged_in", comment: "VoiceOver value for plugged in device")) - } -} - struct BatteryGauge_Previews: PreviewProvider { static var previews: some View { VStack { diff --git a/Meshtastic/Views/Helpers/ConnectedDevice.swift b/Meshtastic/Views/Helpers/ConnectedDevice.swift index 4a46db41..c795b1b0 100644 --- a/Meshtastic/Views/Helpers/ConnectedDevice.swift +++ b/Meshtastic/Views/Helpers/ConnectedDevice.swift @@ -21,46 +21,22 @@ struct ConnectedDevice: View { if (phoneOnly && UIDevice.current.userInterfaceIdiom == .phone) || !phoneOnly { if bluetoothOn { if deviceConnected { - // Create an HStack for connected state with proper accessibility - HStack { - if mqttUplinkEnabled || mqttDownlinkEnabled { - MQTTIcon(connected: mqttProxyConnected, uplink: mqttUplinkEnabled, downlink: mqttDownlinkEnabled, topic: mqttTopic) - .accessibilityHidden(true) - } - Image(systemName: "antenna.radiowaves.left.and.right.circle.fill") - .imageScale(.large) - .foregroundColor(.green) - .symbolRenderingMode(.hierarchical) - .accessibilityHidden(true) - Text(name.addingVariationSelectors) - .font(name.isEmoji() ? .title : .callout) - .foregroundColor(.gray) - .accessibilityHidden(true) - } - .accessibilityElement(children: .ignore) - .accessibilityLabel("bluetooth.connected".localized + ", " + name.formatNodeNameForVoiceOver()) + if mqttUplinkEnabled || mqttDownlinkEnabled { + MQTTIcon(connected: mqttProxyConnected, uplink: mqttUplinkEnabled, downlink: mqttDownlinkEnabled, topic: mqttTopic) + } + Image(systemName: "antenna.radiowaves.left.and.right.circle.fill") + .imageScale(.large) + .foregroundColor(.green) + .symbolRenderingMode(.hierarchical) + Text(name.addingVariationSelectors).font(name.isEmoji() ? .title : .callout).foregroundColor(.gray) } else { - // Create a container for disconnected state - HStack { - Image(systemName: "antenna.radiowaves.left.and.right.slash") - .imageScale(.medium) - .foregroundColor(.red) - .symbolRenderingMode(.hierarchical) - .accessibilityHidden(true) - } - .accessibilityElement(children: .ignore) - .accessibilityLabel("bluetooth.not.connected".localized) + Image(systemName: "antenna.radiowaves.left.and.right.slash") + .imageScale(.medium) + .foregroundColor(.red) + .symbolRenderingMode(.hierarchical) } } else { - // Create a container for Bluetooth off state - HStack { - Text("bluetooth.off".localized) - .font(.subheadline) - .foregroundColor(.red) - .accessibilityHidden(true) - } - .accessibilityElement(children: .ignore) - .accessibilityLabel("bluetooth.off".localized) + Text("Bluetooth is off").font(.subheadline).foregroundColor(.red) } } } diff --git a/Meshtastic/Views/Messages/TextMessageField/RequestPositionButton.swift b/Meshtastic/Views/Messages/TextMessageField/RequestPositionButton.swift index fd166f51..2f1634bc 100644 --- a/Meshtastic/Views/Messages/TextMessageField/RequestPositionButton.swift +++ b/Meshtastic/Views/Messages/TextMessageField/RequestPositionButton.swift @@ -6,7 +6,6 @@ struct RequestPositionButton: View { var body: some View { Button(action: action) { Image(systemName: "mappin.and.ellipse") - .accessibilityLabel(NSLocalizedString("request_position", comment: "VoiceOver label for request position button")) .symbolRenderingMode(.hierarchical) .imageScale(.large) .foregroundColor(.accentColor) diff --git a/Meshtastic/Views/Messages/TextMessageField/TextMessageSize.swift b/Meshtastic/Views/Messages/TextMessageField/TextMessageSize.swift index 9839e246..aacbd60d 100644 --- a/Meshtastic/Views/Messages/TextMessageField/TextMessageSize.swift +++ b/Meshtastic/Views/Messages/TextMessageField/TextMessageSize.swift @@ -6,8 +6,6 @@ struct TextMessageSize: View { var body: some View { ProgressView("\("Bytes".localized): \(totalBytes) / \(maxbytes)", value: Double(totalBytes), total: Double(maxbytes)) - .accessibilityLabel(NSLocalizedString("message_size", comment: "VoiceOver label for message size")) - .accessibilityValue(String(format: NSLocalizedString("bytes_used", comment: "VoiceOver value for bytes used"), totalBytes, maxbytes)) .frame(width: 130) .padding(5) .font(.subheadline) diff --git a/Meshtastic/Views/Nodes/Helpers/Actions/IgnoreNodeButton.swift b/Meshtastic/Views/Nodes/Helpers/Actions/IgnoreNodeButton.swift index 2d73d5c0..84fdf4d3 100644 --- a/Meshtastic/Views/Nodes/Helpers/Actions/IgnoreNodeButton.swift +++ b/Meshtastic/Views/Nodes/Helpers/Actions/IgnoreNodeButton.swift @@ -40,7 +40,6 @@ struct IgnoreNodeButton: View { Image(systemName: node.ignored ? "minus.circle.fill" : "minus.circle") .symbolRenderingMode(.multicolor) } - // Accessibility: Label for VoiceOver } } } diff --git a/Meshtastic/Views/Nodes/Helpers/NodeDetail.swift b/Meshtastic/Views/Nodes/Helpers/NodeDetail.swift index c5670e06..081e7adc 100644 --- a/Meshtastic/Views/Nodes/Helpers/NodeDetail.swift +++ b/Meshtastic/Views/Nodes/Helpers/NodeDetail.swift @@ -46,8 +46,7 @@ struct NodeDetail: View { Section("Hardware") { NodeInfoItem(node: node) } - .accessibilityElement(children: .combine) - Section("Node") { // Node + Section("Node") { HStack(alignment: .center) { Spacer() CircleText( @@ -68,7 +67,6 @@ struct NodeDetail: View { .foregroundColor(getRssiColor(rssi: node.rssi)) .font(.caption) } - .accessibilityElement(children: .combine) } if node.telemetries?.count ?? 0 > 0 { Spacer() @@ -76,7 +74,6 @@ struct NodeDetail: View { } Spacer() } - .accessibilityElement(children: .combine) .listRowSeparator(.hidden) if let user = node.user { if !user.keyMatch { @@ -89,7 +86,6 @@ struct NodeDetail: View { .foregroundStyle(.secondary) .font(.callout) } - .accessibilityElement(children: .combine) } icon: { Image(systemName: "key.slash.fill") .symbolRenderingMode(.multicolor) @@ -108,7 +104,6 @@ struct NodeDetail: View { Text(String(node.num)) .textSelection(.enabled) } - .accessibilityElement(children: .combine) HStack { Label { @@ -121,7 +116,6 @@ struct NodeDetail: View { Text(node.num.toHex()) .textSelection(.enabled) } - .accessibilityElement(children: .combine) if let metadata = node.metadata { HStack { @@ -135,7 +129,6 @@ struct NodeDetail: View { Text(metadata.firmwareVersion ?? "Unknown".localized) } - .accessibilityElement(children: .combine) } if let role = node.user?.role, let deviceRole = DeviceRoles(rawValue: Int(role)) { @@ -149,7 +142,6 @@ struct NodeDetail: View { Spacer() Text(deviceRole.name) } - .accessibilityElement(children: .combine) } if let dm = node.telemetries?.filtered(using: NSPredicate(format: "metricsType == 0")).lastObject as? TelemetryEntity, let uptimeSeconds = dm.uptimeSeconds { @@ -169,7 +161,6 @@ struct NodeDetail: View { Text(uptime) .textSelection(.enabled) } - .accessibilityElement(children: .combine) } if let firstHeard = node.firstHeard, firstHeard.timeIntervalSince1970 > 0 && firstHeard < Calendar.current.date(byAdding: .year, value: 1, to: Date())! { @@ -188,9 +179,7 @@ struct NodeDetail: View { Text(firstHeard.formatted()) .textSelection(.enabled) } - } - .accessibilityElement(children: .combine) - .onTapGesture { + }.onTapGesture { dateFormatRelative.toggle() } } @@ -214,9 +203,7 @@ struct NodeDetail: View { Text(lastHeard.formatted()) .textSelection(.enabled) } - } - .accessibilityElement(children: .combine) - .onTapGesture { + }.onTapGesture { dateFormatRelative.toggle() } } @@ -229,84 +216,79 @@ struct NodeDetail: View { if node.hasPositions && UserDefaults.environmentEnableWeatherKit || node.hasDataForLatestEnvironmentMetrics(attributes: ["iaq", "temperature", "relativeHumidity", "barometricPressure", "windSpeed", "radiation", "weight", "Distance", "soilTemperature", "soilMoisture"]) { Section("Environment") { - // Group weather/environment data for better VoiceOver experience - VStack { - if !node.hasEnvironmentMetrics { - LocalWeatherConditions(location: node.latestPosition?.nodeLocation) - } else { - VStack { - if node.latestEnvironmentMetrics?.iaq ?? -1 > 0 { - IndoorAirQuality(iaq: Int(node.latestEnvironmentMetrics?.iaq ?? 0), displayMode: .gradient) - .padding(.vertical) - } - LazyVGrid(columns: gridItemLayout) { - if let temperature = node.latestEnvironmentMetrics?.temperature?.shortFormattedTemperature() { - WeatherConditionsCompactWidget(temperature: String(temperature), symbolName: "cloud.sun", description: "TEMP") - } - if let humidity = node.latestEnvironmentMetrics?.relativeHumidity { - if let temperature = node.latestEnvironmentMetrics?.temperature { - let dewPoint = calculateDewPoint(temp: temperature, relativeHumidity: humidity) - .formatted(.number.precision(.fractionLength(0))) + "°" - HumidityCompactWidget(humidity: Int(humidity), dewPoint: dewPoint) - } else { - HumidityCompactWidget(humidity: Int(humidity), dewPoint: nil) - } - } - if let pressure = node.latestEnvironmentMetrics?.barometricPressure { - PressureCompactWidget(pressure: pressure.formatted(.number.precision(.fractionLength(2))), unit: "hPA", low: pressure <= 1009.144) - } - if let windSpeed = node.latestEnvironmentMetrics?.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)) - 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) - } - if let rainfall1h = node.latestEnvironmentMetrics?.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 - let unitLabel = usesMetricSystem ? "mm" : "in" - let measurement = Measurement(value: Double(rainfall1h), unit: UnitLength.millimeters) - let decimals = usesMetricSystem ? 0 : 1 - 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 { - 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 - let unitLabel = usesMetricSystem ? "mm" : "in" - let measurement = Measurement(value: Double(rainfall24h), unit: UnitLength.millimeters) - let decimals = usesMetricSystem ? 0 : 1 - 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 { - RadiationCompactWidget(radiation: radiation.formatted(.number.precision(.fractionLength(1))), unit: "µR/hr") - } - if let weight = node.latestEnvironmentMetrics?.weight { - WeightCompactWidget(weight: weight.formatted(.number.precision(.fractionLength(1))), unit: "kg") - } - if let distance = node.latestEnvironmentMetrics?.distance { - DistanceCompactWidget(distance: distance.formatted(.number.precision(.fractionLength(0))), unit: "mm") - } - if let soilTemperature = node.latestEnvironmentMetrics?.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 { - SoilMoistureCompactWidget(moisture: soilMoisture.formatted(.number.precision(.fractionLength(0))), unit: "%") - } - } - .padding(node.latestEnvironmentMetrics?.iaq ?? -1 > 0 ? .bottom : .vertical) + if !node.hasEnvironmentMetrics { + LocalWeatherConditions(location: node.latestPosition?.nodeLocation) + } else { + VStack { + if node.latestEnvironmentMetrics?.iaq ?? -1 > 0 { + IndoorAirQuality(iaq: Int(node.latestEnvironmentMetrics?.iaq ?? 0), displayMode: .gradient) + .padding(.vertical) } + LazyVGrid(columns: gridItemLayout) { + if let temperature = node.latestEnvironmentMetrics?.temperature?.shortFormattedTemperature() { + WeatherConditionsCompactWidget(temperature: String(temperature), symbolName: "cloud.sun", description: "TEMP") + } + if let humidity = node.latestEnvironmentMetrics?.relativeHumidity { + if let temperature = node.latestEnvironmentMetrics?.temperature { + let dewPoint = calculateDewPoint(temp: temperature, relativeHumidity: humidity) + .formatted(.number.precision(.fractionLength(0))) + "°" + HumidityCompactWidget(humidity: Int(humidity), dewPoint: dewPoint) + } else { + HumidityCompactWidget(humidity: Int(humidity), dewPoint: nil) + } + } + if let pressure = node.latestEnvironmentMetrics?.barometricPressure { + PressureCompactWidget(pressure: pressure.formatted(.number.precision(.fractionLength(2))), unit: "hPA", low: pressure <= 1009.144) + } + if let windSpeed = node.latestEnvironmentMetrics?.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)) + 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) + } + if let rainfall1h = node.latestEnvironmentMetrics?.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 + let unitLabel = usesMetricSystem ? "mm" : "in" + let measurement = Measurement(value: Double(rainfall1h), unit: UnitLength.millimeters) + let decimals = usesMetricSystem ? 0 : 1 + 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 { + 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 + let unitLabel = usesMetricSystem ? "mm" : "in" + let measurement = Measurement(value: Double(rainfall24h), unit: UnitLength.millimeters) + let decimals = usesMetricSystem ? 0 : 1 + 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 { + RadiationCompactWidget(radiation: radiation.formatted(.number.precision(.fractionLength(1))), unit: "µR/hr") + } + if let weight = node.latestEnvironmentMetrics?.weight { + WeightCompactWidget(weight: weight.formatted(.number.precision(.fractionLength(1))), unit: "kg") + } + if let distance = node.latestEnvironmentMetrics?.distance { + DistanceCompactWidget(distance: distance.formatted(.number.precision(.fractionLength(0))), unit: "mm") + } + if let soilTemperature = node.latestEnvironmentMetrics?.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 { + SoilMoistureCompactWidget(moisture: soilMoisture.formatted(.number.precision(.fractionLength(0))), unit: "%") + } + } + .padding(node.latestEnvironmentMetrics?.iaq ?? -1 > 0 ? .bottom : .vertical) } } - // Apply accessibility properties to the environment section - .accessibilityElement(children: .combine) } } if node.hasPowerMetrics && node.latestPowerMetrics != nil { @@ -316,7 +298,6 @@ struct NodeDetail: View { PowerMetrics(metric: metric) } } - .accessibilityElement(children: .combine) } } Section("Logs") { diff --git a/Meshtastic/Views/Nodes/Helpers/NodeInfoItem.swift b/Meshtastic/Views/Nodes/Helpers/NodeInfoItem.swift index eb4c37b0..07f3d92c 100644 --- a/Meshtastic/Views/Nodes/Helpers/NodeInfoItem.swift +++ b/Meshtastic/Views/Nodes/Helpers/NodeInfoItem.swift @@ -31,7 +31,6 @@ struct NodeInfoItem: View { .foregroundStyle(.gray) .font(.callout) } - .accessibilityElement(children: .combine) Spacer() } VStack(alignment: .center) { @@ -50,11 +49,9 @@ struct NodeInfoItem: View { .cornerRadius(5) } } - .accessibilityElement(children: .combine) } Spacer() } - .accessibilityElement(children: .combine) .onAppear { Api().loadDeviceHardwareData { (hw) in for device in hw { @@ -82,7 +79,6 @@ struct NodeInfoItem: View { Text(String("incomplete".localized)) } } - .accessibilityElement(children: .combine) } } } diff --git a/Meshtastic/Views/Nodes/Helpers/NodeListItem.swift b/Meshtastic/Views/Nodes/Helpers/NodeListItem.swift index 62dd5fd0..2978ceab 100644 --- a/Meshtastic/Views/Nodes/Helpers/NodeListItem.swift +++ b/Meshtastic/Views/Nodes/Helpers/NodeListItem.swift @@ -7,99 +7,9 @@ import SwiftUI import CoreLocation -import Foundation struct NodeListItem: View { - // Accessibility: Synthesized description for VoiceOver - private var accessibilityDescription: String { - var desc = "" - if let shortName = node.user?.shortName { - // Format the shortName using the String extension method - desc = shortName.formatNodeNameForVoiceOver() - } else if let longName = node.user?.longName { - desc = longName - } else { - desc = "unknown node" - } - if connected { - desc += ", currently connected" - } - if node.favorite { - desc += ", favorite" - } - if node.lastHeard != nil { - let formatter = RelativeDateTimeFormatter() - formatter.unitsStyle = .full - let relative = formatter.localizedString(for: node.lastHeard!, relativeTo: Date()) - desc += ", last heard " + relative - } - if node.isOnline { - desc += ", online" - } else { - desc += ", offline" - } - let role = DeviceRoles(rawValue: Int(node.user?.role ?? 0)) - if let roleName = role?.name { - desc += ", role: \(roleName)" - } - if node.hopsAway > 0 { - desc += ", \(node.hopsAway) hops away" - } - if let battery = node.latestDeviceMetrics?.batteryLevel { - // Check for plugged in and charging states, same logic as in BatteryCompact and BatteryGauge - if battery > 100 { - desc += ", " + NSLocalizedString("device_plugged_in", comment: "VoiceOver value for plugged in device") - } else if battery == 100 { - desc += ", " + NSLocalizedString("device_charging", comment: "VoiceOver value for charging device") - } else { - desc += ", battery \(battery)%" - } - } - // Add distance and heading/bearing if available, but only for non-connected nodes - if !connected, let (lastPosition, myCoord) = locationData { - let nodeCoord = CLLocation(latitude: lastPosition.nodeCoordinate!.latitude, longitude: lastPosition.nodeCoordinate!.longitude) - let metersAway = nodeCoord.distance(from: myCoord) - - // Distance information - let distanceFormatter = LengthFormatter() - distanceFormatter.unitStyle = .medium - let formattedDistance = distanceFormatter.string(fromMeters: metersAway) - // For VoiceOver, prepend 'Distance' (localized) - desc += ", " + String(format: "%@: %@", NSLocalizedString("distance", comment: "Distance label for VoiceOver"), formattedDistance) - - // Add bearing/heading information for VoiceOver - let trueBearing = getBearingBetweenTwoPoints(point1: myCoord, point2: nodeCoord) - let heading = Measurement(value: trueBearing, unit: UnitAngle.degrees) - let formattedHeading = heading.formatted(.measurement(width: .narrow, numberFormatStyle: .number.precision(.fractionLength(0)))) - // Using a direct format without requiring a new localization key - desc += ", " + NSLocalizedString("heading", comment: "Heading label for VoiceOver") + " " + formattedHeading - } - // Add signal strength if available - if node.snr != 0 && !node.viaMqtt { - let signalStrength: BLESignalStrength - if node.snr < -10 { - signalStrength = .weak - } else if node.snr < 5 { - signalStrength = .normal - } else { - signalStrength = .strong - } - let signalString: String - switch signalStrength { - case .weak: - signalString = NSLocalizedString("ble.signal.strength.weak", comment: "VoiceOver value for weak BLE signal strength") - case .normal: - signalString = NSLocalizedString("ble.signal.strength.normal", comment: "VoiceOver value for normal BLE signal strength") - case .strong: - signalString = NSLocalizedString("ble.signal.strength.strong", comment: "VoiceOver value for strong BLE signal strength") - } - desc += ", " + signalString - } - return desc - } - - @ObservedObject var node: NodeInfoEntity var connected: Bool var connectedNode: Int64 @@ -257,10 +167,7 @@ struct NodeListItem: View { } .padding(.top, 4) .padding(.bottom, 4) - // Accessibility: Make the whole row a single element for VoiceOver - .accessibilityElement(children: .ignore) - .accessibilityLabel(accessibilityDescription) - } + } } struct DefaultIcon: View { diff --git a/Meshtastic/Views/Nodes/NodeList.swift b/Meshtastic/Views/Nodes/NodeList.swift index 9af36fbd..a17a19d0 100644 --- a/Meshtastic/Views/Nodes/NodeList.swift +++ b/Meshtastic/Views/Nodes/NodeList.swift @@ -235,8 +235,6 @@ struct NodeList: View { phoneOnly: true ) } - // Make sure the ZStack passes through accessibility to the ConnectedDevice component - .accessibilityElement(children: .contain) ) } content: { if let node = selectedNode { @@ -255,7 +253,6 @@ struct NodeList: View { } label: { Image(systemName: "rectangle") } - .accessibilityLabel("Hide sidebar") } ConnectedDevice( bluetoothOn: bleManager.isSwitchedOn, @@ -264,8 +261,6 @@ struct NodeList: View { phoneOnly: true ) } - // Make sure the ZStack passes through accessibility to the ConnectedDevice component - .accessibilityElement(children: .contain) ) } } else { From 8dfa7f5b1441467736ff494ce185b1e751229d2f Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Tue, 13 May 2025 06:40:50 -0700 Subject: [PATCH 022/213] Bump timestamp up to an hour --- Meshtastic/Extensions/CoreData/MessageEntityExtension.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Meshtastic/Extensions/CoreData/MessageEntityExtension.swift b/Meshtastic/Extensions/CoreData/MessageEntityExtension.swift index 0cfbd579..e7abb191 100644 --- a/Meshtastic/Extensions/CoreData/MessageEntityExtension.swift +++ b/Meshtastic/Extensions/CoreData/MessageEntityExtension.swift @@ -34,7 +34,7 @@ extension MessageEntity { func displayTimestamp(aboveMessage: MessageEntity?) -> Bool { if let aboveMessage = aboveMessage { - return aboveMessage.timestamp.addingTimeInterval(900) < timestamp // 15 minutes + return aboveMessage.timestamp.addingTimeInterval(3600) < timestamp // 60 minutes } return false // First message will have no timestamp } From 1b701ba40d460b3dbb72a60d27fe37720a5da4c9 Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Tue, 13 May 2025 07:35:49 -0700 Subject: [PATCH 023/213] Fix logging type --- Localizable.xcstrings | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/Localizable.xcstrings b/Localizable.xcstrings index c62f4e6a..b97eb676 100644 --- a/Localizable.xcstrings +++ b/Localizable.xcstrings @@ -22467,8 +22467,7 @@ } } }, - "Positon config received: %@" : { - "extractionState" : "stale", + "Position config received: %@" : { "localizations" : { "de" : { "stringUnit" : { From 980debd8e8529f0ba6ab90afa2e673e3d1a46aa6 Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Tue, 13 May 2025 10:01:44 -0700 Subject: [PATCH 024/213] Auto favorite node when sending a DM --- Meshtastic/Helpers/BLEManager.swift | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/Meshtastic/Helpers/BLEManager.swift b/Meshtastic/Helpers/BLEManager.swift index b5971896..e26d6245 100644 --- a/Meshtastic/Helpers/BLEManager.swift +++ b/Meshtastic/Helpers/BLEManager.swift @@ -1120,6 +1120,19 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate if newMessage.toUser?.pkiEncrypted ?? false { meshPacket.pkiEncrypted = true meshPacket.publicKey = newMessage.toUser?.publicKey ?? Data() + // Auto Favorite nodes you DM so they don't roll out of the nodedb + if !(newMessage.toUser?.userNode?.favorite ?? true) { + newMessage.toUser?.userNode?.favorite = true + do { + try context.save() + Logger.data.info("💾 Auto favorited node bases on sending a message \(self.connectedPeripheral.num.toHex(), privacy: .public) to \(toUserNum.toHex(), privacy: .public)") + _ = self.setFavoriteNode(node: (newMessage.toUser?.userNode)!, connectedNodeNum: fromUserNum) + } catch { + context.rollback() + let nsError = error as NSError + Logger.data.error("Unresolved Core Data error when auto favoriting in Send Message Function. Error: \(nsError, privacy: .public)") + } + } } meshPacket.id = UInt32(newMessage.messageId) if toUserNum > 0 { From e64a01e72fe4287db2dc0cfa132f21f3b11e92d6 Mon Sep 17 00:00:00 2001 From: Ben Meadors Date: Tue, 13 May 2025 16:59:02 -0500 Subject: [PATCH 025/213] Protobufs --- .../Sources/meshtastic/admin.pb.swift | 423 +++++--------- .../Sources/meshtastic/apponly.pb.swift | 8 +- .../Sources/meshtastic/atak.pb.swift | 69 +-- .../meshtastic/cannedmessages.pb.swift | 8 +- .../Sources/meshtastic/channel.pb.swift | 38 +- .../Sources/meshtastic/clientonly.pb.swift | 8 +- .../Sources/meshtastic/config.pb.swift | 488 +++++++--------- .../meshtastic/connection_status.pb.swift | 23 +- .../Sources/meshtastic/device_ui.pb.swift | 41 +- .../Sources/meshtastic/deviceonly.pb.swift | 33 +- .../Sources/meshtastic/interdevice.pb.swift | 64 +-- .../Sources/meshtastic/localonly.pb.swift | 11 +- .../Sources/meshtastic/mesh.pb.swift | 527 +++++------------- .../Sources/meshtastic/module_config.pb.swift | 298 ++++------ .../Sources/meshtastic/mqtt.pb.swift | 11 +- .../Sources/meshtastic/paxcount.pb.swift | 8 +- .../Sources/meshtastic/portnums.pb.swift | 16 +- .../Sources/meshtastic/powermon.pb.swift | 117 ++-- .../meshtastic/remote_hardware.pb.swift | 37 +- .../Sources/meshtastic/rtttl.pb.swift | 8 +- .../Sources/meshtastic/storeforward.pb.swift | 94 +--- .../Sources/meshtastic/telemetry.pb.swift | 83 +-- .../Sources/meshtastic/xmodem.pb.swift | 40 +- protobufs | 2 +- 24 files changed, 767 insertions(+), 1688 deletions(-) diff --git a/MeshtasticProtobufs/Sources/meshtastic/admin.pb.swift b/MeshtasticProtobufs/Sources/meshtastic/admin.pb.swift index e450d566..3f259682 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: @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. @@ -487,6 +488,16 @@ public struct AdminMessage { set {payloadVariant = .commitEditSettings(newValue)} } + /// + /// Add a contact (User) to the nodedb + public var addContact: SharedContact { + get { + if case .addContact(let v)? = payloadVariant {return v} + return SharedContact() + } + set {payloadVariant = .addContact(newValue)} + } + /// /// Tell the node to factory reset config everything; all device state and configuration will be returned to factory defaults and BLE bonds will be cleared. public var factoryResetDevice: Int32 { @@ -563,7 +574,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) @@ -705,6 +716,9 @@ public struct AdminMessage { /// Commits an open transaction for any edits made to config, module config, owner, and channel settings case commitEditSettings(Bool) /// + /// Add a contact (User) to the nodedb + case addContact(SharedContact) + /// /// Tell the node to factory reset config everything; all device state and configuration will be returned to factory defaults and BLE bonds will be cleared. case factoryResetDevice(Int32) /// @@ -728,225 +742,11 @@ public struct AdminMessage { /// Tell the node to reset the nodedb. case nodedbReset(Int32) - #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 (.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 (.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 /// @@ -1026,11 +826,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 /// @@ -1128,9 +942,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 /// @@ -1162,61 +993,20 @@ public struct AdminMessage { } } + // The compiler won't synthesize support with the UNRECOGNIZED case. + public static let allCases: [AdminMessage.BackupLocation] = [ + .flash, + .sd, + ] + } 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. @@ -1246,7 +1036,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. @@ -1260,15 +1050,32 @@ public struct NodeRemoteHardwarePinsResponse { public init() {} } -#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 HamParameters: @unchecked Sendable {} -extension NodeRemoteHardwarePinsResponse: @unchecked Sendable {} -#endif // swift(>=5.5) && canImport(_Concurrency) +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. + + /// + /// The node number of the contact + public var nodeNum: UInt32 = 0 + + /// + /// The User of the contact + public var user: User { + get {return _user ?? User()} + set {_user = newValue} + } + /// Returns true if `user` has been explicitly set. + public var hasUser: Bool {return self._user != nil} + /// Clears the value of `user`. Subsequent reads from it will return its default value. + public mutating func clearUser() {self._user = nil} + + public var unknownFields = SwiftProtobuf.UnknownStorage() + + public init() {} + + fileprivate var _user: User? = nil +} // MARK: - Code below here is support for the SwiftProtobuf runtime. @@ -1322,6 +1129,7 @@ extension AdminMessage: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementat 48: .standard(proto: "remove_ignored_node"), 64: .standard(proto: "begin_edit_settings"), 65: .standard(proto: "commit_edit_settings"), + 66: .standard(proto: "add_contact"), 94: .standard(proto: "factory_reset_device"), 95: .standard(proto: "reboot_ota_seconds"), 96: .standard(proto: "exit_simulator"), @@ -1764,6 +1572,19 @@ extension AdminMessage: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementat self.payloadVariant = .commitEditSettings(v) } }() + case 66: try { + var v: SharedContact? + var hadOneofValue = false + if let current = self.payloadVariant { + hadOneofValue = true + if case .addContact(let m) = current {v = m} + } + try decoder.decodeSingularMessageField(value: &v) + if let v = v { + if hadOneofValue {try decoder.handleConflictingOneOf()} + self.payloadVariant = .addContact(v) + } + }() case 94: try { var v: Int32? try decoder.decodeSingularInt32Field(value: &v) @@ -2008,6 +1829,10 @@ extension AdminMessage: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementat guard case .commitEditSettings(let v)? = self.payloadVariant else { preconditionFailure() } try visitor.visitSingularBoolField(value: v, fieldNumber: 65) }() + case .addContact?: try { + guard case .addContact(let v)? = self.payloadVariant else { preconditionFailure() } + try visitor.visitSingularMessageField(value: v, fieldNumber: 66) + }() case .factoryResetDevice?: try { guard case .factoryResetDevice(let v)? = self.payloadVariant else { preconditionFailure() } try visitor.visitSingularInt32Field(value: v, fieldNumber: 94) @@ -2123,7 +1948,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 { @@ -2173,3 +1998,45 @@ extension NodeRemoteHardwarePinsResponse: SwiftProtobuf.Message, SwiftProtobuf._ return true } } + +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"), + ] + + 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.decodeSingularUInt32Field(value: &self.nodeNum) }() + case 2: try { try decoder.decodeSingularMessageField(value: &self._user) }() + default: break + } + } + } + + public func traverse(visitor: inout V) throws { + // The use of inline closures is to circumvent an issue where the compiler + // allocates stack space for every if/case branch local when no optimizations + // are enabled. https://github.com/apple/swift-protobuf/issues/1034 and + // https://github.com/apple/swift-protobuf/issues/1182 + if self.nodeNum != 0 { + try visitor.visitSingularUInt32Field(value: self.nodeNum, fieldNumber: 1) + } + try { if let v = self._user { + try visitor.visitSingularMessageField(value: v, fieldNumber: 2) + } }() + try unknownFields.traverse(visitor: &visitor) + } + + public static func ==(lhs: SharedContact, rhs: SharedContact) -> Bool { + if lhs.nodeNum != rhs.nodeNum {return false} + if lhs._user != rhs._user {return false} + if lhs.unknownFields != rhs.unknownFields {return false} + return true + } +} diff --git a/MeshtasticProtobufs/Sources/meshtastic/apponly.pb.swift b/MeshtasticProtobufs/Sources/meshtastic/apponly.pb.swift index 0457077c..52dac5ca 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,10 +53,6 @@ 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" diff --git a/MeshtasticProtobufs/Sources/meshtastic/atak.pb.swift b/MeshtasticProtobufs/Sources/meshtastic/atak.pb.swift index 867648a9..06d6af88 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: @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. @@ -337,7 +326,7 @@ public struct TAKPacket { /// /// The payload of the packet - public enum OneOf_PayloadVariant: Equatable { + public enum OneOf_PayloadVariant: Equatable, @unchecked 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,18 +482,6 @@ 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" diff --git a/MeshtasticProtobufs/Sources/meshtastic/cannedmessages.pb.swift b/MeshtasticProtobufs/Sources/meshtastic/cannedmessages.pb.swift index 1b8c84de..ce1f0503 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,10 +36,6 @@ 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" diff --git a/MeshtasticProtobufs/Sources/meshtastic/channel.pb.swift b/MeshtasticProtobufs/Sources/meshtastic/channel.pb.swift index 5b9c7e49..180cd698 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: @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. /// /// Deprecated in favor of LoraConfig.channel_num + /// + /// NOTE: This field was marked as deprecated in the .proto file. public var channelNum: UInt32 = 0 /// @@ -111,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. @@ -132,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. @@ -170,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 /// @@ -209,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() {} @@ -216,26 +226,6 @@ 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" diff --git a/MeshtasticProtobufs/Sources/meshtastic/clientonly.pb.swift b/MeshtasticProtobufs/Sources/meshtastic/clientonly.pb.swift index f89a8e3c..d72c0ae1 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,10 +130,6 @@ 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" diff --git a/MeshtasticProtobufs/Sources/meshtastic/config.pb.swift b/MeshtasticProtobufs/Sources/meshtastic/config.pb.swift index e0d45bcf..55e6e5f4 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 /// @@ -238,7 +193,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 /// @@ -256,6 +211,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 /// @@ -356,11 +313,27 @@ 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, + ] + } /// /// 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 /// @@ -421,6 +394,16 @@ 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, + ] + } public init() {} @@ -428,7 +411,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. @@ -450,6 +433,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 /// @@ -460,6 +445,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 /// @@ -500,7 +487,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 /// @@ -590,9 +577,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 /// @@ -630,6 +632,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() {} @@ -638,7 +647,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. @@ -698,7 +707,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. @@ -749,7 +758,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 /// @@ -781,11 +790,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 /// @@ -817,9 +832,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. @@ -852,7 +873,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. @@ -913,7 +934,7 @@ public struct Config { /// /// 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 /// @@ -976,11 +997,21 @@ public struct Config { } } + // The compiler won't synthesize support with the UNRECOGNIZED case. + public static let allCases: [Config.DisplayConfig.GpsCoordinateFormat] = [ + .dec, + .dms, + .utm, + .mgrs, + .olc, + .osgr, + ] + } /// /// Unit display preference - public enum DisplayUnits: SwiftProtobuf.Enum { + public enum DisplayUnits: SwiftProtobuf.Enum, Swift.CaseIterable { public typealias RawValue = Int /// @@ -1012,11 +1043,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 /// @@ -1066,9 +1103,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, + .oledSh110712864, + ] + } - public enum DisplayMode: SwiftProtobuf.Enum { + public enum DisplayMode: SwiftProtobuf.Enum, Swift.CaseIterable { public typealias RawValue = Int /// @@ -1112,9 +1158,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 /// @@ -1182,6 +1236,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() {} @@ -1189,7 +1255,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. @@ -1353,7 +1419,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 /// @@ -1505,12 +1571,38 @@ 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, + ] + } /// /// 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 /// @@ -1524,6 +1616,8 @@ public struct Config { /// /// 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 /// @@ -1587,6 +1681,19 @@ public struct Config { } } + // 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, + ] + } public init() {} @@ -1594,7 +1701,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. @@ -1613,7 +1720,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 /// @@ -1651,12 +1758,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: @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. @@ -1700,7 +1814,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. @@ -1713,218 +1827,6 @@ 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, - ] -} - -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.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.GpsCoordinateFormat: CaseIterable { - // The compiler won't synthesize support with the UNRECOGNIZED case. - public static let allCases: [Config.DisplayConfig.GpsCoordinateFormat] = [ - .dec, - .dms, - .utm, - .mgrs, - .olc, - .osgr, - ] -} - -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, - .oledSh110712864, - ] -} - -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, - ] -} - -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.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.GpsCoordinateFormat: @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" @@ -2432,7 +2334,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 { @@ -2900,7 +2802,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 { @@ -2924,7 +2826,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 { @@ -3141,8 +3043,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..6847c0e3 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,15 +209,6 @@ 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" diff --git a/MeshtasticProtobufs/Sources/meshtastic/device_ui.pb.swift b/MeshtasticProtobufs/Sources/meshtastic/device_ui.pb.swift index c3835518..637b20a8 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 Theme: SwiftProtobuf.Enum { +public enum Theme: SwiftProtobuf.Enum, Swift.CaseIterable { public typealias RawValue = Int /// @@ -58,24 +59,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 /// @@ -209,11 +204,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, @@ -236,11 +226,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. @@ -361,7 +350,7 @@ public struct DeviceUIConfig { fileprivate var _storage = _StorageClass.defaultInstance } -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. @@ -399,7 +388,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. @@ -429,7 +418,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. @@ -451,7 +440,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. @@ -482,16 +471,6 @@ public struct Map { fileprivate var _home: GeoPoint? = nil } -#if swift(>=5.5) && canImport(_Concurrency) -extension Theme: @unchecked Sendable {} -extension Language: @unchecked Sendable {} -extension DeviceUIConfig: @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" diff --git a/MeshtasticProtobufs/Sources/meshtastic/deviceonly.pb.swift b/MeshtasticProtobufs/Sources/meshtastic/deviceonly.pb.swift index 9a5dfe8f..72248719 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: @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. /// /// This is the addr of the radio. + /// + /// NOTE: This field was marked as deprecated in the .proto file. public var macaddr: Data = Data() /// @@ -102,7 +105,7 @@ public struct UserLite { public init() {} } -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. @@ -224,7 +227,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. @@ -284,6 +287,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} @@ -292,6 +297,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} @@ -324,7 +331,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. @@ -346,7 +353,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. @@ -368,7 +375,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. @@ -435,16 +442,6 @@ 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" @@ -680,7 +677,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 { diff --git a/MeshtasticProtobufs/Sources/meshtastic/interdevice.pb.swift b/MeshtasticProtobufs/Sources/meshtastic/interdevice.pb.swift index 92b72c15..165ed685 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,41 +166,15 @@ 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" diff --git a/MeshtasticProtobufs/Sources/meshtastic/localonly.pb.swift b/MeshtasticProtobufs/Sources/meshtastic/localonly.pb.swift index 0af27466..c3356286 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,11 +293,6 @@ 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" diff --git a/MeshtasticProtobufs/Sources/meshtastic/mesh.pb.swift b/MeshtasticProtobufs/Sources/meshtastic/mesh.pb.swift index 051f395f..14948e13 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 /// @@ -662,11 +663,6 @@ public enum HardwareModel: SwiftProtobuf.Enum { } } -} - -#if swift(>=4.2) - -extension HardwareModel: CaseIterable { // The compiler won't synthesize support with the UNRECOGNIZED case. public static let allCases: [HardwareModel] = [ .unset, @@ -769,13 +765,12 @@ extension HardwareModel: CaseIterable { .crowpanel, .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 /// @@ -810,26 +805,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 /// @@ -938,11 +927,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, @@ -960,15 +944,14 @@ extension CriticalErrorCode: CaseIterable { .flashCorruptionRecoverable, .flashCorruptionUnrecoverable, ] -} -#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 /// @@ -1084,11 +1067,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, @@ -1108,13 +1086,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. @@ -1331,7 +1308,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 /// @@ -1375,12 +1352,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 /// @@ -1430,6 +1415,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() {} @@ -1437,31 +1431,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. @@ -1483,7 +1452,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: @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. @@ -1508,6 +1477,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() /// @@ -1539,7 +1510,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. @@ -1567,7 +1538,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. @@ -1607,7 +1578,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) @@ -1619,34 +1590,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 /// @@ -1764,42 +1713,36 @@ 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, + ] + } 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, - ] -} - -#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: @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. @@ -1866,7 +1809,7 @@ public struct DataMessage { /// /// 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. @@ -1928,7 +1871,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: @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. @@ -1969,7 +1912,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, @unchecked Sendable { /// /// Bytes case data(Data) @@ -1977,24 +1920,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() {} @@ -2004,7 +1929,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. @@ -2138,6 +2063,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} @@ -2199,7 +2126,7 @@ public struct MeshPacket { public var unknownFields = SwiftProtobuf.UnknownStorage() - public enum OneOf_PayloadVariant: Equatable { + public enum OneOf_PayloadVariant: Equatable, @unchecked Sendable { /// /// TODO: REPLACE case decoded(DataMessage) @@ -2207,24 +2134,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 } /// @@ -2246,7 +2155,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 /// @@ -2330,11 +2239,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 /// @@ -2372,6 +2295,13 @@ public struct MeshPacket { } } + // The compiler won't synthesize support with the UNRECOGNIZED case. + public static let allCases: [MeshPacket.Delayed] = [ + .noDelay, + .broadcast, + .direct, + ] + } public init() {} @@ -2379,35 +2309,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, - ] -} - -#endif // swift(>=4.2) - /// /// The bluetooth to device link: /// Old BTLE protocol docs from TODO, merge in above and make real docs... @@ -2425,7 +2326,7 @@ extension MeshPacket.Delayed: 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. @@ -2538,7 +2439,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: @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. @@ -2577,7 +2478,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. @@ -2602,7 +2503,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 /// @@ -2664,29 +2565,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. @@ -2713,7 +2608,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. @@ -2899,7 +2794,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) @@ -2957,80 +2852,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() {} @@ -3041,7 +2862,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. @@ -3078,7 +2899,7 @@ public struct ClientNotification { /// /// 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. @@ -3099,7 +2920,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. @@ -3179,7 +3000,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) @@ -3206,40 +3027,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() {} @@ -3247,7 +3034,7 @@ public struct ToRadio { /// /// Compressed message payload -public struct Compressed { +public struct Compressed: @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. @@ -3267,7 +3054,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. @@ -3295,7 +3082,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. @@ -3325,7 +3112,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. @@ -3387,7 +3174,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. @@ -3399,7 +3186,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. @@ -3426,7 +3213,7 @@ public struct NodeRemoteHardwarePin { fileprivate var _pin: RemoteHardwarePin? = nil } -public struct ChunkedPayload { +public struct ChunkedPayload: @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. @@ -3454,7 +3241,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. @@ -3468,7 +3255,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. @@ -3511,7 +3298,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) @@ -3522,77 +3309,11 @@ 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 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 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 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 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" @@ -4654,7 +4375,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 { @@ -4856,7 +4577,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 { @@ -5735,7 +5456,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 { @@ -5860,8 +5581,8 @@ extension Heartbeat: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementation 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/module_config.pb.swift b/MeshtasticProtobufs/Sources/meshtastic/module_config.pb.swift index bcf4041c..f717951d 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. @@ -420,7 +352,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. @@ -444,7 +376,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. @@ -470,7 +402,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. @@ -517,7 +449,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 @@ -569,6 +501,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() {} @@ -576,7 +518,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. @@ -613,7 +555,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 @@ -660,6 +602,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() {} @@ -667,7 +622,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. @@ -693,7 +648,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. @@ -736,7 +691,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 @@ -804,11 +759,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 @@ -821,6 +796,10 @@ public struct ModuleConfig { /// Ecowitt WS85 weather station case ws85 // = 6 + + /// VE.Direct is a serial protocol used by Victron Energy products + /// https://beta.ivc.no/wiki/index.php/Victron_VE_Direct_DIY_Cable + case veDirect // = 7 case UNRECOGNIZED(Int) public init() { @@ -836,6 +815,7 @@ public struct ModuleConfig { case 4: self = .nmea case 5: self = .caltopo case 6: self = .ws85 + case 7: self = .veDirect default: self = .UNRECOGNIZED(rawValue) } } @@ -849,10 +829,23 @@ public struct ModuleConfig { case .nmea: return 4 case .caltopo: return 5 case .ws85: return 6 + case .veDirect: return 7 case .UNRECOGNIZED(let i): return i } } + // 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, + ] + } public init() {} @@ -860,7 +853,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. @@ -943,7 +936,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. @@ -979,7 +972,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. @@ -1004,7 +997,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. @@ -1073,7 +1066,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. @@ -1128,7 +1121,7 @@ public struct ModuleConfig { /// /// TODO: REPLACE - public enum InputEventChar: SwiftProtobuf.Enum { + public enum InputEventChar: SwiftProtobuf.Enum, Swift.CaseIterable { public typealias RawValue = Int /// @@ -1196,6 +1189,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() {} @@ -1204,7 +1209,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. @@ -1237,89 +1242,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, - ] -} - -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. @@ -1341,32 +1266,6 @@ 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" @@ -2190,6 +2089,7 @@ extension ModuleConfig.SerialConfig.Serial_Mode: SwiftProtobuf._ProtoNameProvidi 4: .same(proto: "NMEA"), 5: .same(proto: "CALTOPO"), 6: .same(proto: "WS85"), + 7: .same(proto: "VE_DIRECT"), ] } diff --git a/MeshtasticProtobufs/Sources/meshtastic/mqtt.pb.swift b/MeshtasticProtobufs/Sources/meshtastic/mqtt.pb.swift index efe6cdd5..006fd9c8 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. @@ -121,11 +121,6 @@ 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" diff --git a/MeshtasticProtobufs/Sources/meshtastic/paxcount.pb.swift b/MeshtasticProtobufs/Sources/meshtastic/paxcount.pb.swift index cf8aa463..e24ed371 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,10 +44,6 @@ 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" diff --git a/MeshtasticProtobufs/Sources/meshtastic/portnums.pb.swift b/MeshtasticProtobufs/Sources/meshtastic/portnums.pb.swift index 3b0efa08..cac96bc4 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 /// @@ -290,11 +290,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, @@ -328,14 +323,9 @@ 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 { diff --git a/MeshtasticProtobufs/Sources/meshtastic/powermon.pb.swift b/MeshtasticProtobufs/Sources/meshtastic/powermon.pb.swift index 5f51e948..58c21701 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 { @@ -379,7 +360,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) diff --git a/MeshtasticProtobufs/Sources/meshtastic/remote_hardware.pb.swift b/MeshtasticProtobufs/Sources/meshtastic/remote_hardware.pb.swift index ac6eeb26..d23dc07b 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,32 +110,21 @@ 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" diff --git a/MeshtasticProtobufs/Sources/meshtastic/rtttl.pb.swift b/MeshtasticProtobufs/Sources/meshtastic/rtttl.pb.swift index 6fdf3208..38d0c880 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,10 +36,6 @@ 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" diff --git a/MeshtasticProtobufs/Sources/meshtastic/storeforward.pb.swift b/MeshtasticProtobufs/Sources/meshtastic/storeforward.pb.swift index 54efa77b..deb96569 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: @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. @@ -79,7 +80,7 @@ public struct StoreAndForward { /// /// TODO: REPLACE - public enum OneOf_Variant: Equatable { + public enum OneOf_Variant: Equatable, @unchecked 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,41 +335,6 @@ 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" diff --git a/MeshtasticProtobufs/Sources/meshtastic/telemetry.pb.swift b/MeshtasticProtobufs/Sources/meshtastic/telemetry.pb.swift index da282fe2..90b56546 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 /// @@ -270,11 +270,6 @@ public enum TelemetrySensorType: SwiftProtobuf.Enum { } } -} - -#if swift(>=4.2) - -extension TelemetrySensorType: CaseIterable { // The compiler won't synthesize support with the UNRECOGNIZED case. public static let allCases: [TelemetrySensorType] = [ .sensorUnset, @@ -316,13 +311,12 @@ extension TelemetrySensorType: CaseIterable { .dps310, .rak12035, ] -} -#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. @@ -395,7 +389,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. @@ -653,7 +647,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. @@ -738,7 +732,7 @@ public struct PowerMetrics { /// /// Air quality metrics -public struct AirQualityMetrics { +public struct AirQualityMetrics: 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 +901,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. @@ -965,7 +959,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. @@ -1014,7 +1008,7 @@ public struct HealthMetrics { /// /// Types of Measurements the telemetry module is equipped to handle -public struct Telemetry { +public struct Telemetry: 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. @@ -1087,7 +1081,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) @@ -1107,40 +1101,6 @@ public struct Telemetry { /// Health telemetry metrics case healthMetrics(HealthMetrics) - #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 - }() - default: return false - } - } - #endif } public init() {} @@ -1148,7 +1108,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. @@ -1166,19 +1126,6 @@ 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 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" @@ -1746,10 +1693,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 { @@ -2016,7 +1963,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..46907a58 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: @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. @@ -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,34 +80,23 @@ 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" diff --git a/protobufs b/protobufs index 06864665..816595c8 160000 --- a/protobufs +++ b/protobufs @@ -1 +1 @@ -Subproject commit 068646653e8375fc145988026ad242a3cf70f7ab +Subproject commit 816595c8bbdfc3b4388e11348ccd043294d58705 From 05397db3a4a4339fb626a19c7e2ae95dc6316100 Mon Sep 17 00:00:00 2001 From: Ben Meadors Date: Tue, 13 May 2025 19:43:43 -0500 Subject: [PATCH 026/213] Add shared contact functionality (WIP) Feel free to hijack and make it not terrible --- Meshtastic.xcodeproj/project.pbxproj | 8 ++ .../CoreData/NodeInfoEntityToNodeInfo.swift | 26 ++++++ Meshtastic/Helpers/BLEManager.swift | 49 ++++++++++ Meshtastic/Meshtastic.entitlements | 1 + Meshtastic/MeshtasticApp.swift | 61 ++++++++++++- .../Nodes/Helpers/ShareContactQRDialog.swift | 91 +++++++++++++++++++ Meshtastic/Views/Nodes/NodeList.swift | 13 +++ 7 files changed, 247 insertions(+), 2 deletions(-) create mode 100644 Meshtastic/Extensions/CoreData/NodeInfoEntityToNodeInfo.swift create mode 100644 Meshtastic/Views/Nodes/Helpers/ShareContactQRDialog.swift diff --git a/Meshtastic.xcodeproj/project.pbxproj b/Meshtastic.xcodeproj/project.pbxproj index 66649df5..7f4c9e0d 100644 --- a/Meshtastic.xcodeproj/project.pbxproj +++ b/Meshtastic.xcodeproj/project.pbxproj @@ -7,6 +7,8 @@ objects = { /* Begin PBXBuildFile section */ + 108FFECB2DD3F43C00BFAA81 /* ShareContactQRDialog.swift in Sources */ = {isa = PBXBuildFile; fileRef = 108FFECA2DD3F43C00BFAA81 /* ShareContactQRDialog.swift */; }; + 108FFECD2DD4005600BFAA81 /* NodeInfoEntityToNodeInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 108FFECC2DD4005600BFAA81 /* NodeInfoEntityToNodeInfo.swift */; }; 231B3F212D087A4C0069A07D /* MetricTableColumn.swift in Sources */ = {isa = PBXBuildFile; fileRef = 231B3F202D087A4C0069A07D /* MetricTableColumn.swift */; }; 231B3F222D087A4C0069A07D /* MetricsColumnList.swift in Sources */ = {isa = PBXBuildFile; fileRef = 231B3F1F2D087A4C0069A07D /* MetricsColumnList.swift */; }; 231B3F252D087C3C0069A07D /* EnvironmentDefaultColumns.swift in Sources */ = {isa = PBXBuildFile; fileRef = 231B3F242D087C3C0069A07D /* EnvironmentDefaultColumns.swift */; }; @@ -274,6 +276,8 @@ /* End PBXCopyFilesBuildPhase section */ /* Begin PBXFileReference section */ + 108FFECA2DD3F43C00BFAA81 /* ShareContactQRDialog.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShareContactQRDialog.swift; sourceTree = ""; }; + 108FFECC2DD4005600BFAA81 /* NodeInfoEntityToNodeInfo.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NodeInfoEntityToNodeInfo.swift; sourceTree = ""; }; 231B3F1F2D087A4C0069A07D /* MetricsColumnList.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MetricsColumnList.swift; sourceTree = ""; }; 231B3F202D087A4C0069A07D /* MetricTableColumn.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MetricTableColumn.swift; sourceTree = ""; }; 231B3F242D087C3C0069A07D /* EnvironmentDefaultColumns.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EnvironmentDefaultColumns.swift; sourceTree = ""; }; @@ -701,6 +705,7 @@ DD007BB12AA59B9A00F5FA12 /* CoreData */ = { isa = PBXGroup; children = ( + 108FFECC2DD4005600BFAA81 /* NodeInfoEntityToNodeInfo.swift */, 2344A2AA2D66973D00170A77 /* ManagedAttributePropertyWrapper.swift */, DD58C5F12919AD3C00D5BEFB /* ChannelEntityExtension.swift */, 6DEDA55B2A9592F900321D2E /* MessageEntityExtension.swift */, @@ -1101,6 +1106,7 @@ DDDB26402AABEF7B003AFCB7 /* Helpers */ = { isa = PBXGroup; children = ( + 108FFECA2DD3F43C00BFAA81 /* ShareContactQRDialog.swift */, 231B3F232D087C020069A07D /* Metrics Columns */, DDAD49EB2AFAE82500B4425D /* Map */, DDDB26432AAC0206003AFCB7 /* NodeDetail.swift */, @@ -1396,6 +1402,7 @@ DD4640202AFF10F4002A5ECB /* WaypointForm.swift in Sources */, 233E99C12D849D6000CC3A77 /* DistanceCompactWidget.swift in Sources */, DD769E0328D18BF1001A3F05 /* DeviceMetricsLog.swift in Sources */, + 108FFECD2DD4005600BFAA81 /* NodeInfoEntityToNodeInfo.swift in Sources */, DDAF8C5326EB1DF10058C060 /* BLEManager.swift in Sources */, DD15E4F32B8BA56E00654F61 /* PaxCounterConfig.swift in Sources */, DDDB445229F8ACF900EE2349 /* Date.swift in Sources */, @@ -1486,6 +1493,7 @@ DDA1C48E28DB49D3009933EC /* ChannelRoles.swift in Sources */, DDD5BB092C285DDC007E03CA /* AppLog.swift in Sources */, DD8ED9C8289CE4B900B3B0AB /* RoutingError.swift in Sources */, + 108FFECB2DD3F43C00BFAA81 /* ShareContactQRDialog.swift in Sources */, 233E99C52D84A0B600CC3A77 /* CompactWidget.swift in Sources */, DDC1B81A2AB5377B00C71E39 /* MessagesTips.swift in Sources */, DD964FC62975DBFD007C176F /* QueryCoreData.swift in Sources */, diff --git a/Meshtastic/Extensions/CoreData/NodeInfoEntityToNodeInfo.swift b/Meshtastic/Extensions/CoreData/NodeInfoEntityToNodeInfo.swift new file mode 100644 index 00000000..c34f62a0 --- /dev/null +++ b/Meshtastic/Extensions/CoreData/NodeInfoEntityToNodeInfo.swift @@ -0,0 +1,26 @@ +// NodeInfoEntityToNodeInfo.swift +// Meshtastic +// +// Utility to convert NodeInfoEntity (Core Data) to NodeInfo (protobuf) + +import Foundation +import MeshtasticProtobufs + +extension NodeInfoEntity { + func toProto() -> NodeInfo { + var userProto = User() + if let user = self.user { + userProto.id = user.userId ?? "" + userProto.longName = user.longName ?? "" + userProto.shortName = user.shortName ?? "" + userProto.hwModel = HardwareModel(rawValue: Int(user.hwModelId))!; userProto.isLicensed = user.isLicensed + userProto.role = Config.DeviceConfig.Role(rawValue: Int(user.role))! + userProto.publicKey = user.publicKey?.subdata(in: 0.. Bool { + if isConnected { + + let decodedString = base64UrlString.base64urlToBase64() + if let decodedData = Data(base64Encoded: decodedString) { + do { + let contact: SharedContact = try SharedContact(serializedBytes: decodedData) + var adminPacket = AdminMessage() + adminPacket.addContact = contact + var meshPacket: MeshPacket = MeshPacket() + meshPacket.to = UInt32(connectedPeripheral.num) + meshPacket.from = UInt32(connectedPeripheral.num) + meshPacket.id = UInt32.random(in: UInt32(UInt8.max).. Int64 { var adminPacket = AdminMessage() adminPacket.setOwner = config diff --git a/Meshtastic/Meshtastic.entitlements b/Meshtastic/Meshtastic.entitlements index abcef61d..0d2247ee 100644 --- a/Meshtastic/Meshtastic.entitlements +++ b/Meshtastic/Meshtastic.entitlements @@ -7,6 +7,7 @@ com.apple.developer.associated-domains applinks:meshtastic.org/e/* + applinks:meshtastic.org/v/* com.apple.developer.weatherkit diff --git a/Meshtastic/MeshtasticApp.swift b/Meshtastic/MeshtasticApp.swift index 5c82c257..7ba8b701 100644 --- a/Meshtastic/MeshtasticApp.swift +++ b/Meshtastic/MeshtasticApp.swift @@ -4,6 +4,7 @@ import SwiftUI import CoreData import OSLog import TipKit +import MeshtasticProtobufs @main struct MeshtasticAppleApp: App { @@ -87,7 +88,63 @@ struct MeshtasticAppleApp: App { Logger.mesh.debug("Some sort of URL was received \(url, privacy: .public)") self.incomingUrl = url - if url.absoluteString.lowercased().contains("meshtastic.org/e/#") { + if url.absoluteString.lowercased().contains("meshtastic.org/v/#") { + if let components = self.incomingUrl?.absoluteString.components(separatedBy: "#") { + // Extract contact information from the URL + if let contactData = components.last { + + let decodedString = contactData.base64urlToBase64() + if let decodedData = Data(base64Encoded: decodedString) { + do { + let contact = try MeshtasticProtobufs.SharedContact(serializedBytes: decodedData) + + // Show an alert to confirm adding the contact + let alertController = UIAlertController( + title: "Add Contact", + message: "Would you like to add \(contact.user.longName) as a contact?", + preferredStyle: .alert + ) + + alertController.addAction(UIAlertAction( + title: "Yes", + style: .default, + handler: { _ in + let success = BLEManager.shared.addContactFromURL(base64UrlString: contactData) + Logger.services.debug("Contact added from URL: \(success ? "success" : "failed")") + } + )) + + alertController.addAction(UIAlertAction( + title: "No", + style: .cancel, + handler: nil + )) + + // Present the alert + if let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene, + let rootViewController = windowScene.windows.first?.rootViewController { + rootViewController.present(alertController, animated: true) + } + Logger.services.debug("Contact data extracted from URL: \(contactData, privacy: .public)") + } catch { + Logger.services.error("Failed to parse contact data: \(error.localizedDescription, privacy: .public)") + + // Show error alert to user + if let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene, + let rootViewController = windowScene.windows.first?.rootViewController { + let errorAlert = UIAlertController( + title: "Error", + message: "Could not process contact information. Invalid format.", + preferredStyle: .alert + ) + errorAlert.addAction(UIAlertAction(title: "OK", style: .default)) + rootViewController.present(errorAlert, animated: true) + } + } + } + } + } + } 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 if self.incomingUrl?.absoluteString.lowercased().contains("?") != nil { @@ -119,7 +176,7 @@ struct MeshtasticAppleApp: App { .displayFrequency(.immediate) ] ) - } + } } .onChange(of: scenePhase) { (_, newScenePhase) in switch newScenePhase { diff --git a/Meshtastic/Views/Nodes/Helpers/ShareContactQRDialog.swift b/Meshtastic/Views/Nodes/Helpers/ShareContactQRDialog.swift new file mode 100644 index 00000000..23b65ce6 --- /dev/null +++ b/Meshtastic/Views/Nodes/Helpers/ShareContactQRDialog.swift @@ -0,0 +1,91 @@ +// ShareContactQRDialog.swift +// Meshtastic +// +// Created by GitHub Copilot on 5/13/25. + +import SwiftUI +import CoreImage.CIFilterBuiltins +#if canImport(UIKit) +import UIKit +#endif +import CoreData +import MeshtasticProtobufs +import OSLog + +struct ShareContactQRDialog: View { + let node: NodeInfo + @Environment(\.dismiss) private var dismiss + + var qrString: String { + var contact = SharedContact() + contact.nodeNum = node.num + contact.user = node.user + + do { + let contactString = try contact.serializedData().base64EncodedString() + return ("https://meshtastic.org/v/#" + contactString.base64ToBase64url()) + } catch { + Logger.services.error("Error serializing contact: \(error)") + return "Error generating QR code" + } + + } + + var qrImage: UIImage { + let context = CIContext() + let filter = CIFilter.qrCodeGenerator() + filter.setValue(Data(qrString.utf8), forKey: "inputMessage") + let transform = CGAffineTransform(scaleX: 10, y: 10) + if let outputImage = filter.outputImage?.transformed(by: transform), + let cgimg = context.createCGImage(outputImage, from: outputImage.extent) { + return UIImage(cgImage: cgimg) + } + return UIImage(systemName: "xmark.circle") ?? UIImage() + } + + var body: some View { + VStack(spacing: 20) { + Text("Share Contact QR") + .font(.title2) + .padding(.top) + Text(node.user.longName) + .font(.headline) + Image(uiImage: qrImage) + .interpolation(.none) + .resizable() + .scaledToFit() + .frame(width: 220, height: 220) + .background(Color(.systemBackground)) + .cornerRadius(16) + .shadow(radius: 4) + Text("Scan this QR code to add \(node.user.longName) to another device.") + .font(.subheadline) + .multilineTextAlignment(.center) + .foregroundColor(.secondary) + Button("Done") { dismiss() } + .buttonStyle(.borderedProminent) + .padding(.bottom) + } + .padding() + .frame(maxWidth: 350) + } +} + +#if DEBUG +struct ShareContactQRDialog_Previews: PreviewProvider { + static var previews: some View { + var node = NodeInfo() + node.num = 123456 + var userProto = User() + userProto.id = "!1234" + userProto.longName = "Bud" + userProto.shortName = "Bud" + userProto.hwModel = HardwareModel(rawValue:1)!; + userProto.role = Config.DeviceConfig.Role(rawValue: 1)! + userProto.publicKey = Data() + node.user = userProto + + return ShareContactQRDialog(node: node) + } +} +#endif diff --git a/Meshtastic/Views/Nodes/NodeList.swift b/Meshtastic/Views/Nodes/NodeList.swift index a17a19d0..168aa018 100644 --- a/Meshtastic/Views/Nodes/NodeList.swift +++ b/Meshtastic/Views/Nodes/NodeList.swift @@ -40,6 +40,8 @@ struct NodeList: View { @State private var isPresentingPositionFailedAlert = false @State private var isPresentingDeleteNodeAlert = false @State private var deleteNodeId: Int64 = 0 + @State private var isPresentingShareContactQR = false + @State private var shareContactNode: NodeInfoEntity? var boolFilters: [Bool] {[ isFavorite, @@ -78,6 +80,12 @@ struct NodeList: View { /// Allow users to mute notifications for a node even if they are not connected if let user = node.user { NodeAlertsButton(context: context, node: node, user: user) + Button(action: { + shareContactNode = node + isPresentingShareContactQR = true + }) { + Label("Share Contact QR", systemImage: "qrcode") + } } if let connectedNode { /// Favoriting a node requires being connected @@ -223,6 +231,11 @@ struct NodeList: View { } } } + } + .sheet(isPresented: $isPresentingShareContactQR) { + if let node = shareContactNode { + ShareContactQRDialog(node: node.toProto()) + } } .navigationSplitViewColumnWidth(min: 100, ideal: 250, max: 500) .navigationBarItems( From 6fc6a8fcfa78f7b1a529d076fe464e5787d640f3 Mon Sep 17 00:00:00 2001 From: Ben Meadors Date: Tue, 13 May 2025 19:59:12 -0500 Subject: [PATCH 027/213] Fix QR code generation being slow as ball and log error --- Meshtastic/Helpers/BLEManager.swift | 1 + Meshtastic/Views/Nodes/Helpers/ShareContactQRDialog.swift | 1 - 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/Meshtastic/Helpers/BLEManager.swift b/Meshtastic/Helpers/BLEManager.swift index 5da739ae..cb759ffb 100644 --- a/Meshtastic/Helpers/BLEManager.swift +++ b/Meshtastic/Helpers/BLEManager.swift @@ -1811,6 +1811,7 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate } } catch { + Logger.data.error("Failed to decode contact data: \(error.localizedDescription, privacy: .public)") return false } } diff --git a/Meshtastic/Views/Nodes/Helpers/ShareContactQRDialog.swift b/Meshtastic/Views/Nodes/Helpers/ShareContactQRDialog.swift index 23b65ce6..30f63747 100644 --- a/Meshtastic/Views/Nodes/Helpers/ShareContactQRDialog.swift +++ b/Meshtastic/Views/Nodes/Helpers/ShareContactQRDialog.swift @@ -54,7 +54,6 @@ struct ShareContactQRDialog: View { .interpolation(.none) .resizable() .scaledToFit() - .frame(width: 220, height: 220) .background(Color(.systemBackground)) .cornerRadius(16) .shadow(radius: 4) From 9a5b3d7c65ea271edb77c5e88f003c1701cd6c28 Mon Sep 17 00:00:00 2001 From: Ben Meadors Date: Tue, 13 May 2025 20:02:40 -0500 Subject: [PATCH 028/213] Update Meshtastic/Views/Nodes/NodeList.swift Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- Meshtastic/Views/Nodes/NodeList.swift | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Meshtastic/Views/Nodes/NodeList.swift b/Meshtastic/Views/Nodes/NodeList.swift index 168aa018..370f03eb 100644 --- a/Meshtastic/Views/Nodes/NodeList.swift +++ b/Meshtastic/Views/Nodes/NodeList.swift @@ -235,6 +235,8 @@ struct NodeList: View { .sheet(isPresented: $isPresentingShareContactQR) { if let node = shareContactNode { ShareContactQRDialog(node: node.toProto()) + } else { + EmptyView() } } .navigationSplitViewColumnWidth(min: 100, ideal: 250, max: 500) From bb43a1ec9da7895cd61dfcc291d7f02c248db01c Mon Sep 17 00:00:00 2001 From: Ben Meadors Date: Tue, 13 May 2025 20:03:33 -0500 Subject: [PATCH 029/213] Update Meshtastic/Views/Nodes/Helpers/ShareContactQRDialog.swift Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- Meshtastic/Views/Nodes/Helpers/ShareContactQRDialog.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Meshtastic/Views/Nodes/Helpers/ShareContactQRDialog.swift b/Meshtastic/Views/Nodes/Helpers/ShareContactQRDialog.swift index 30f63747..76901ff6 100644 --- a/Meshtastic/Views/Nodes/Helpers/ShareContactQRDialog.swift +++ b/Meshtastic/Views/Nodes/Helpers/ShareContactQRDialog.swift @@ -26,7 +26,7 @@ struct ShareContactQRDialog: View { return ("https://meshtastic.org/v/#" + contactString.base64ToBase64url()) } catch { Logger.services.error("Error serializing contact: \(error)") - return "Error generating QR code" + return "" } } From 1b00ea786069aa6b47dc4559111d82a1fa82850b Mon Sep 17 00:00:00 2001 From: Ben Meadors Date: Tue, 13 May 2025 20:03:44 -0500 Subject: [PATCH 030/213] Update Meshtastic/MeshtasticApp.swift Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- Meshtastic/MeshtasticApp.swift | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Meshtastic/MeshtasticApp.swift b/Meshtastic/MeshtasticApp.swift index 7ba8b701..5d8df782 100644 --- a/Meshtastic/MeshtasticApp.swift +++ b/Meshtastic/MeshtasticApp.swift @@ -89,9 +89,9 @@ struct MeshtasticAppleApp: App { Logger.mesh.debug("Some sort of URL was received \(url, privacy: .public)") self.incomingUrl = url if url.absoluteString.lowercased().contains("meshtastic.org/v/#") { - if let components = self.incomingUrl?.absoluteString.components(separatedBy: "#") { - // Extract contact information from the URL - if let contactData = components.last { + let components = self.incomingUrl?.absoluteString.components(separatedBy: "#") ?? [] + // Extract contact information from the URL + if let contactData = components.last { let decodedString = contactData.base64urlToBase64() if let decodedData = Data(base64Encoded: decodedString) { From 2857ed3dc9f74c81df21143382096cb3e14af190 Mon Sep 17 00:00:00 2001 From: Ben Meadors Date: Tue, 13 May 2025 20:03:58 -0500 Subject: [PATCH 031/213] Update Meshtastic/Extensions/CoreData/NodeInfoEntityToNodeInfo.swift Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- Meshtastic/Extensions/CoreData/NodeInfoEntityToNodeInfo.swift | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Meshtastic/Extensions/CoreData/NodeInfoEntityToNodeInfo.swift b/Meshtastic/Extensions/CoreData/NodeInfoEntityToNodeInfo.swift index c34f62a0..935ad956 100644 --- a/Meshtastic/Extensions/CoreData/NodeInfoEntityToNodeInfo.swift +++ b/Meshtastic/Extensions/CoreData/NodeInfoEntityToNodeInfo.swift @@ -13,7 +13,8 @@ extension NodeInfoEntity { userProto.id = user.userId ?? "" userProto.longName = user.longName ?? "" userProto.shortName = user.shortName ?? "" - userProto.hwModel = HardwareModel(rawValue: Int(user.hwModelId))!; userProto.isLicensed = user.isLicensed + userProto.hwModel = HardwareModel(rawValue: Int(user.hwModelId))! + userProto.isLicensed = user.isLicensed userProto.role = Config.DeviceConfig.Role(rawValue: Int(user.role))! userProto.publicKey = user.publicKey?.subdata(in: 0.. Date: Tue, 13 May 2025 20:06:07 -0500 Subject: [PATCH 032/213] Copilot jacked up my braces --- Meshtastic/MeshtasticApp.swift | 1 - 1 file changed, 1 deletion(-) diff --git a/Meshtastic/MeshtasticApp.swift b/Meshtastic/MeshtasticApp.swift index 5d8df782..1a4ba85b 100644 --- a/Meshtastic/MeshtasticApp.swift +++ b/Meshtastic/MeshtasticApp.swift @@ -143,7 +143,6 @@ struct MeshtasticAppleApp: App { } } } - } } 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 From bf55f3526de146e2bb1ad3eafecc8f515208835a Mon Sep 17 00:00:00 2001 From: Ben Meadors Date: Tue, 13 May 2025 20:09:30 -0500 Subject: [PATCH 033/213] Update Meshtastic/Extensions/CoreData/NodeInfoEntityToNodeInfo.swift Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- Meshtastic/Extensions/CoreData/NodeInfoEntityToNodeInfo.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Meshtastic/Extensions/CoreData/NodeInfoEntityToNodeInfo.swift b/Meshtastic/Extensions/CoreData/NodeInfoEntityToNodeInfo.swift index 935ad956..a1a0a03c 100644 --- a/Meshtastic/Extensions/CoreData/NodeInfoEntityToNodeInfo.swift +++ b/Meshtastic/Extensions/CoreData/NodeInfoEntityToNodeInfo.swift @@ -15,7 +15,7 @@ extension NodeInfoEntity { userProto.shortName = user.shortName ?? "" userProto.hwModel = HardwareModel(rawValue: Int(user.hwModelId))! userProto.isLicensed = user.isLicensed - userProto.role = Config.DeviceConfig.Role(rawValue: Int(user.role))! + userProto.role = Config.DeviceConfig.Role(rawValue: Int(user.role)) ?? .unknown userProto.publicKey = user.publicKey?.subdata(in: 0.. Date: Tue, 13 May 2025 20:09:35 -0500 Subject: [PATCH 034/213] Update Meshtastic/Extensions/CoreData/NodeInfoEntityToNodeInfo.swift Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- Meshtastic/Extensions/CoreData/NodeInfoEntityToNodeInfo.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Meshtastic/Extensions/CoreData/NodeInfoEntityToNodeInfo.swift b/Meshtastic/Extensions/CoreData/NodeInfoEntityToNodeInfo.swift index a1a0a03c..05f3ec3b 100644 --- a/Meshtastic/Extensions/CoreData/NodeInfoEntityToNodeInfo.swift +++ b/Meshtastic/Extensions/CoreData/NodeInfoEntityToNodeInfo.swift @@ -13,7 +13,7 @@ extension NodeInfoEntity { userProto.id = user.userId ?? "" userProto.longName = user.longName ?? "" userProto.shortName = user.shortName ?? "" - userProto.hwModel = HardwareModel(rawValue: Int(user.hwModelId))! + userProto.hwModel = HardwareModel(rawValue: Int(user.hwModelId)) ?? .unknown userProto.isLicensed = user.isLicensed userProto.role = Config.DeviceConfig.Role(rawValue: Int(user.role)) ?? .unknown userProto.publicKey = user.publicKey?.subdata(in: 0.. Date: Tue, 13 May 2025 20:11:27 -0500 Subject: [PATCH 035/213] Defaults --- Meshtastic/Extensions/CoreData/NodeInfoEntityToNodeInfo.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Meshtastic/Extensions/CoreData/NodeInfoEntityToNodeInfo.swift b/Meshtastic/Extensions/CoreData/NodeInfoEntityToNodeInfo.swift index 05f3ec3b..73706d2e 100644 --- a/Meshtastic/Extensions/CoreData/NodeInfoEntityToNodeInfo.swift +++ b/Meshtastic/Extensions/CoreData/NodeInfoEntityToNodeInfo.swift @@ -13,9 +13,9 @@ extension NodeInfoEntity { userProto.id = user.userId ?? "" userProto.longName = user.longName ?? "" userProto.shortName = user.shortName ?? "" - userProto.hwModel = HardwareModel(rawValue: Int(user.hwModelId)) ?? .unknown + userProto.hwModel = HardwareModel(rawValue: Int(user.hwModelId)) ?? .unset userProto.isLicensed = user.isLicensed - userProto.role = Config.DeviceConfig.Role(rawValue: Int(user.role)) ?? .unknown + userProto.role = Config.DeviceConfig.Role(rawValue: Int(user.role)) ?? .client userProto.publicKey = user.publicKey?.subdata(in: 0.. Date: Tue, 13 May 2025 19:56:06 -0700 Subject: [PATCH 036/213] Import Contact App Shortcut and add a share url button to the page --- Localizable.xcstrings | 116 +++++++++++------- Meshtastic.xcodeproj/project.pbxproj | 4 + Meshtastic/AppIntents/AddContactIntent.swift | 51 ++++++++ .../Nodes/Helpers/ShareContactQRDialog.swift | 7 ++ 4 files changed, 132 insertions(+), 46 deletions(-) create mode 100644 Meshtastic/AppIntents/AddContactIntent.swift diff --git a/Localizable.xcstrings b/Localizable.xcstrings index b97eb676..23cb27cf 100644 --- a/Localizable.xcstrings +++ b/Localizable.xcstrings @@ -7201,6 +7201,9 @@ } } } + }, + "Contact URL" : { + }, "Contacts (%@)" : { "localizations" : { @@ -9994,6 +9997,9 @@ } } } + }, + "Done" : { + }, "Double Tap as Button" : { "localizations" : { @@ -15082,6 +15088,12 @@ } } } + }, + "Import Contact" : { + + }, + "Import Meshtastic Node %@ as a contact" : { + }, "Import Route" : { "localizations" : { @@ -22193,6 +22205,52 @@ } } }, + "Position config received: %@" : { + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Positionskonfiguration empfangen: %@" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Configuration de la position reçue : %@" + } + }, + "he" : { + "stringUnit" : { + "state" : "translated", + "value" : "הגדרות מיקום התקבלו: %@" + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Configurazione della posizione ricevuta: %@" + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Odebrano konfigurację pozycji: %@" + } + }, + "se" : { + "stringUnit" : { + "state" : "translated", + "value" : "Positionskonfiguration mottagen: %@" + } + }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Конфигурација позиције примљена: %@" + } + } + } + }, "Position Exchange Failed" : { "localizations" : { "it" : { @@ -22467,52 +22525,6 @@ } } }, - "Position config received: %@" : { - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Positionskonfiguration empfangen: %@" - } - }, - "fr" : { - "stringUnit" : { - "state" : "translated", - "value" : "Configuration de la position reçue : %@" - } - }, - "he" : { - "stringUnit" : { - "state" : "translated", - "value" : "הגדרות מיקום התקבלו: %@" - } - }, - "it" : { - "stringUnit" : { - "state" : "translated", - "value" : "Configurazione della posizione ricevuta: %@" - } - }, - "pl" : { - "stringUnit" : { - "state" : "translated", - "value" : "Odebrano konfigurację pozycji: %@" - } - }, - "se" : { - "stringUnit" : { - "state" : "translated", - "value" : "Positionskonfiguration mottagen: %@" - } - }, - "sr" : { - "stringUnit" : { - "state" : "translated", - "value" : "Конфигурација позиције примљена: %@" - } - } - } - }, "Power" : { "localizations" : { "de" : { @@ -26021,6 +26033,9 @@ } } } + }, + "Scan this QR code to add %@ to another device." : { + }, "Screen on for" : { "localizations" : { @@ -28033,6 +28048,9 @@ } } } + }, + "Share Contact QR" : { + }, "Share QR Code" : { "localizations" : { @@ -29711,6 +29729,9 @@ } } } + }, + "Takes a Meshtastic contact URL and saves it to the nodes database" : { + }, "Tapback" : { "localizations" : { @@ -30891,6 +30912,9 @@ } } } + }, + "The URL for the node to import" : { + }, "There has been no response to a request for device metadata over the admin channel for this node." : { "localizations" : { diff --git a/Meshtastic.xcodeproj/project.pbxproj b/Meshtastic.xcodeproj/project.pbxproj index 7f4c9e0d..fb48dfe1 100644 --- a/Meshtastic.xcodeproj/project.pbxproj +++ b/Meshtastic.xcodeproj/project.pbxproj @@ -58,6 +58,7 @@ 8D3F8A412D44C2A6009EAAA4 /* PowerMetricsLog.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8D3F8A402D44C2A6009EAAA4 /* PowerMetricsLog.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 */; }; BC47C2EF2CE0017D008245CA /* MessageNodeIntent.swift in Sources */ = {isa = PBXBuildFile; fileRef = BC47C2EE2CE0017D008245CA /* MessageNodeIntent.swift */; }; BC6B45FF2CB2F98900723CEB /* SaveChannelSettingsIntent.swift in Sources */ = {isa = PBXBuildFile; fileRef = BC6B45FE2CB2F98900723CEB /* SaveChannelSettingsIntent.swift */; }; BCB613812C67290800485544 /* SendWaypointIntent.swift in Sources */ = {isa = PBXBuildFile; fileRef = BCB613802C67290800485544 /* SendWaypointIntent.swift */; }; @@ -322,6 +323,7 @@ 8D3F8A402D44C2A6009EAAA4 /* PowerMetricsLog.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PowerMetricsLog.swift; sourceTree = ""; }; B399E8A32B6F486400E4488E /* RetryButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RetryButton.swift; sourceTree = ""; }; B3E905B02B71F7F300654D07 /* TextMessageField.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TextMessageField.swift; sourceTree = ""; }; + BC10380E2DD4333C00B00BFA /* AddContactIntent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddContactIntent.swift; sourceTree = ""; }; BC47C2EE2CE0017D008245CA /* MessageNodeIntent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageNodeIntent.swift; sourceTree = ""; }; BC5EBA3B2D002A2000C442FF /* MessageNodeIntent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageNodeIntent.swift; sourceTree = ""; }; BC6B45FE2CB2F98900723CEB /* SaveChannelSettingsIntent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SaveChannelSettingsIntent.swift; sourceTree = ""; }; @@ -676,6 +678,7 @@ BCB6137F2C6728E700485544 /* AppIntents */ = { isa = PBXGroup; children = ( + BC10380E2DD4333C00B00BFA /* AddContactIntent.swift */, BC5EBA3B2D002A2000C442FF /* MessageNodeIntent.swift */, BCB613802C67290800485544 /* SendWaypointIntent.swift */, BCB613822C672A2600485544 /* MessageChannelIntent.swift */, @@ -1551,6 +1554,7 @@ 2344A2AB2D66974300170A77 /* ManagedAttributePropertyWrapper.swift in Sources */, BCB613832C672A2600485544 /* MessageChannelIntent.swift in Sources */, D93068D52B812B700066FBC8 /* MessageDestination.swift in Sources */, + BC10380F2DD4334400B00BFA /* AddContactIntent.swift in Sources */, DDA9515E2BC6F56F00CEA535 /* IndoorAirQuality.swift in Sources */, DDDB444E29F8AB0E00EE2349 /* Int.swift in Sources */, DD3CC6BC28E366DF00FA9159 /* Meshtastic.xcdatamodeld in Sources */, diff --git a/Meshtastic/AppIntents/AddContactIntent.swift b/Meshtastic/AppIntents/AddContactIntent.swift new file mode 100644 index 00000000..ff8ca149 --- /dev/null +++ b/Meshtastic/AppIntents/AddContactIntent.swift @@ -0,0 +1,51 @@ +// +// AddContactIntent.swift +// Meshtastic +// +// Created by Benjamin Faershtein on 5/13/25. +// + +import AppIntents +import MeshtasticProtobufs + +struct AddContactIntent: AppIntent { + static var title: LocalizedStringResource = "Import Contact" + static var description: IntentDescription = "Takes a Meshtastic contact URL and saves it to the nodes database" + + @Parameter(title: "Contact URL", description: "The URL for the node to import") + var contactUrl: URL + + // Define the function that performs the main logic + func perform() async throws -> some IntentResult { + // Ensure the BLE Manager is connected + if !BLEManager.shared.isConnected { + throw AppIntentErrors.AppIntentError.notConnected + } + + if contactUrl.absoluteString.lowercased().contains("meshtastic.org/v/#") { + + let components = self.contactUrl.absoluteString.components(separatedBy: "#") + // Extract contact information from the URL + if let contactData = components.last { + + let decodedString = contactData.base64urlToBase64() + if let decodedData = Data(base64Encoded: decodedString) { + do { + let success = BLEManager.shared.addContactFromURL(base64UrlString: contactData) + if !success { + throw AppIntentErrors.AppIntentError.message("Failed to import contact") + } + + } catch { + throw AppIntentErrors.AppIntentError.message("Failed to parse contact data: \(error.localizedDescription)") + + } + } + } + // Return a success result + return .result() + } else { + throw AppIntentErrors.AppIntentError.message("The URL is not a valid Meshtastic contact link") + } + } +} diff --git a/Meshtastic/Views/Nodes/Helpers/ShareContactQRDialog.swift b/Meshtastic/Views/Nodes/Helpers/ShareContactQRDialog.swift index 76901ff6..e5d679b9 100644 --- a/Meshtastic/Views/Nodes/Helpers/ShareContactQRDialog.swift +++ b/Meshtastic/Views/Nodes/Helpers/ShareContactQRDialog.swift @@ -61,6 +61,13 @@ struct ShareContactQRDialog: View { .font(.subheadline) .multilineTextAlignment(.center) .foregroundColor(.secondary) + ShareLink("Share QR Code & Link", + item: Image(uiImage: qrImage), + subject: Text("Import Meshtastic Node \(node.user.shortName) as a contact"), + message: Text(qrString), + preview: SharePreview("Import Meshtastic Node \(node.user.shortName) as a contact", + image: Image(uiImage: qrImage)) + ) Button("Done") { dismiss() } .buttonStyle(.borderedProminent) .padding(.bottom) From 863f51b697e496a6202a79514ebc2027ac3d0293 Mon Sep 17 00:00:00 2001 From: Ben Meadors Date: Wed, 14 May 2025 15:42:38 -0500 Subject: [PATCH 037/213] Update ShareContactQRDialog.swift --- Meshtastic/Views/Nodes/Helpers/ShareContactQRDialog.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Meshtastic/Views/Nodes/Helpers/ShareContactQRDialog.swift b/Meshtastic/Views/Nodes/Helpers/ShareContactQRDialog.swift index e5d679b9..5d361fbe 100644 --- a/Meshtastic/Views/Nodes/Helpers/ShareContactQRDialog.swift +++ b/Meshtastic/Views/Nodes/Helpers/ShareContactQRDialog.swift @@ -63,9 +63,9 @@ struct ShareContactQRDialog: View { .foregroundColor(.secondary) ShareLink("Share QR Code & Link", item: Image(uiImage: qrImage), - subject: Text("Import Meshtastic Node \(node.user.shortName) as a contact"), + subject: Text("Add Meshtastic Node \(node.user.shortName) as a contact"), message: Text(qrString), - preview: SharePreview("Import Meshtastic Node \(node.user.shortName) as a contact", + preview: SharePreview("Add Meshtastic Node \(node.user.shortName) as a contact", image: Image(uiImage: qrImage)) ) Button("Done") { dismiss() } From de3f834c0999e4e2796f94e30fd9d6bcb3dd6c44 Mon Sep 17 00:00:00 2001 From: Ben Meadors Date: Wed, 14 May 2025 15:48:49 -0500 Subject: [PATCH 038/213] Updated protobufs --- .../Sources/meshtastic/deviceonly.pb.swift | 23 +++++++++++++++++++ .../Sources/meshtastic/mesh.pb.swift | 23 +++++++++++++++++++ .../Sources/meshtastic/module_config.pb.swift | 10 ++++++++ .../Sources/meshtastic/mqtt.pb.swift | 11 +++++++++ .../Sources/meshtastic/telemetry.pb.swift | 8 +++++++ protobufs | 2 +- 6 files changed, 76 insertions(+), 1 deletion(-) diff --git a/MeshtasticProtobufs/Sources/meshtastic/deviceonly.pb.swift b/MeshtasticProtobufs/Sources/meshtastic/deviceonly.pb.swift index 72248719..cbcbda13 100644 --- a/MeshtasticProtobufs/Sources/meshtastic/deviceonly.pb.swift +++ b/MeshtasticProtobufs/Sources/meshtastic/deviceonly.pb.swift @@ -100,9 +100,22 @@ public struct UserLite: @unchecked Sendable { /// This is sent out to other nodes on the mesh to allow them to compute a shared secret key. public var publicKey: Data = Data() + /// + /// Whether or not the node can be messaged + public var isUnmessagable: Bool { + get {return _isUnmessagable ?? false} + set {_isUnmessagable = newValue} + } + /// Returns true if `isUnmessagable` has been explicitly set. + public var hasIsUnmessagable: Bool {return self._isUnmessagable != nil} + /// Clears the value of `isUnmessagable`. Subsequent reads from it will return its default value. + public mutating func clearIsUnmessagable() {self._isUnmessagable = nil} + public var unknownFields = SwiftProtobuf.UnknownStorage() public init() {} + + fileprivate var _isUnmessagable: Bool? = nil } public struct NodeInfoLite: @unchecked Sendable { @@ -512,6 +525,7 @@ extension UserLite: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationB 5: .standard(proto: "is_licensed"), 6: .same(proto: "role"), 7: .standard(proto: "public_key"), + 9: .standard(proto: "is_unmessagable"), ] public mutating func decodeMessage(decoder: inout D) throws { @@ -527,12 +541,17 @@ extension UserLite: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationB case 5: try { try decoder.decodeSingularBoolField(value: &self.isLicensed) }() case 6: try { try decoder.decodeSingularEnumField(value: &self.role) }() case 7: try { try decoder.decodeSingularBytesField(value: &self.publicKey) }() + case 9: try { try decoder.decodeSingularBoolField(value: &self._isUnmessagable) }() default: break } } } public func traverse(visitor: inout V) throws { + // The use of inline closures is to circumvent an issue where the compiler + // allocates stack space for every if/case branch local when no optimizations + // are enabled. https://github.com/apple/swift-protobuf/issues/1034 and + // https://github.com/apple/swift-protobuf/issues/1182 if !self.macaddr.isEmpty { try visitor.visitSingularBytesField(value: self.macaddr, fieldNumber: 1) } @@ -554,6 +573,9 @@ extension UserLite: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationB if !self.publicKey.isEmpty { try visitor.visitSingularBytesField(value: self.publicKey, fieldNumber: 7) } + try { if let v = self._isUnmessagable { + try visitor.visitSingularBoolField(value: v, fieldNumber: 9) + } }() try unknownFields.traverse(visitor: &visitor) } @@ -565,6 +587,7 @@ extension UserLite: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationB if lhs.isLicensed != rhs.isLicensed {return false} if lhs.role != rhs.role {return false} if lhs.publicKey != rhs.publicKey {return false} + if lhs._isUnmessagable != rhs._isUnmessagable {return false} if lhs.unknownFields != rhs.unknownFields {return false} return true } diff --git a/MeshtasticProtobufs/Sources/meshtastic/mesh.pb.swift b/MeshtasticProtobufs/Sources/meshtastic/mesh.pb.swift index 14948e13..d59ec2ed 100644 --- a/MeshtasticProtobufs/Sources/meshtastic/mesh.pb.swift +++ b/MeshtasticProtobufs/Sources/meshtastic/mesh.pb.swift @@ -1503,9 +1503,22 @@ public struct User: @unchecked Sendable { /// This is sent out to other nodes on the mesh to allow them to compute a shared secret key. public var publicKey: Data = Data() + /// + /// Whether or not the node can be messaged + public var isUnmessagable: Bool { + get {return _isUnmessagable ?? false} + set {_isUnmessagable = newValue} + } + /// Returns true if `isUnmessagable` has been explicitly set. + public var hasIsUnmessagable: Bool {return self._isUnmessagable != nil} + /// Clears the value of `isUnmessagable`. Subsequent reads from it will return its default value. + public mutating func clearIsUnmessagable() {self._isUnmessagable = nil} + public var unknownFields = SwiftProtobuf.UnknownStorage() public init() {} + + fileprivate var _isUnmessagable: Bool? = nil } /// @@ -3751,6 +3764,7 @@ extension User: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, 6: .standard(proto: "is_licensed"), 7: .same(proto: "role"), 8: .standard(proto: "public_key"), + 9: .standard(proto: "is_unmessagable"), ] public mutating func decodeMessage(decoder: inout D) throws { @@ -3767,12 +3781,17 @@ extension User: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, case 6: try { try decoder.decodeSingularBoolField(value: &self.isLicensed) }() case 7: try { try decoder.decodeSingularEnumField(value: &self.role) }() case 8: try { try decoder.decodeSingularBytesField(value: &self.publicKey) }() + case 9: try { try decoder.decodeSingularBoolField(value: &self._isUnmessagable) }() default: break } } } public func traverse(visitor: inout V) throws { + // The use of inline closures is to circumvent an issue where the compiler + // allocates stack space for every if/case branch local when no optimizations + // are enabled. https://github.com/apple/swift-protobuf/issues/1034 and + // https://github.com/apple/swift-protobuf/issues/1182 if !self.id.isEmpty { try visitor.visitSingularStringField(value: self.id, fieldNumber: 1) } @@ -3797,6 +3816,9 @@ extension User: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, if !self.publicKey.isEmpty { try visitor.visitSingularBytesField(value: self.publicKey, fieldNumber: 8) } + try { if let v = self._isUnmessagable { + try visitor.visitSingularBoolField(value: v, fieldNumber: 9) + } }() try unknownFields.traverse(visitor: &visitor) } @@ -3809,6 +3831,7 @@ extension User: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, if lhs.isLicensed != rhs.isLicensed {return false} if lhs.role != rhs.role {return false} if lhs.publicKey != rhs.publicKey {return false} + if lhs._isUnmessagable != rhs._isUnmessagable {return false} if lhs.unknownFields != rhs.unknownFields {return false} return true } diff --git a/MeshtasticProtobufs/Sources/meshtastic/module_config.pb.swift b/MeshtasticProtobufs/Sources/meshtastic/module_config.pb.swift index f717951d..c2e81366 100644 --- a/MeshtasticProtobufs/Sources/meshtastic/module_config.pb.swift +++ b/MeshtasticProtobufs/Sources/meshtastic/module_config.pb.swift @@ -345,6 +345,10 @@ public struct ModuleConfig: Sendable { /// Bits of precision for the location sent (default of 32 is full precision). public var positionPrecision: UInt32 = 0 + /// + /// Whether we have opted-in to report our location to the map + public var shouldReportLocation: Bool = false + public var unknownFields = SwiftProtobuf.UnknownStorage() public init() {} @@ -1647,6 +1651,7 @@ extension ModuleConfig.MapReportSettings: SwiftProtobuf.Message, SwiftProtobuf._ 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 mutating func decodeMessage(decoder: inout D) throws { @@ -1657,6 +1662,7 @@ extension ModuleConfig.MapReportSettings: SwiftProtobuf.Message, SwiftProtobuf._ switch fieldNumber { case 1: try { try decoder.decodeSingularUInt32Field(value: &self.publishIntervalSecs) }() case 2: try { try decoder.decodeSingularUInt32Field(value: &self.positionPrecision) }() + case 3: try { try decoder.decodeSingularBoolField(value: &self.shouldReportLocation) }() default: break } } @@ -1669,12 +1675,16 @@ extension ModuleConfig.MapReportSettings: SwiftProtobuf.Message, SwiftProtobuf._ if self.positionPrecision != 0 { try visitor.visitSingularUInt32Field(value: self.positionPrecision, fieldNumber: 2) } + if self.shouldReportLocation != false { + try visitor.visitSingularBoolField(value: self.shouldReportLocation, fieldNumber: 3) + } try unknownFields.traverse(visitor: &visitor) } public static func ==(lhs: ModuleConfig.MapReportSettings, rhs: ModuleConfig.MapReportSettings) -> Bool { if lhs.publishIntervalSecs != rhs.publishIntervalSecs {return false} if lhs.positionPrecision != rhs.positionPrecision {return false} + if lhs.shouldReportLocation != rhs.shouldReportLocation {return false} if lhs.unknownFields != rhs.unknownFields {return false} return true } diff --git a/MeshtasticProtobufs/Sources/meshtastic/mqtt.pb.swift b/MeshtasticProtobufs/Sources/meshtastic/mqtt.pb.swift index 006fd9c8..80508b5d 100644 --- a/MeshtasticProtobufs/Sources/meshtastic/mqtt.pb.swift +++ b/MeshtasticProtobufs/Sources/meshtastic/mqtt.pb.swift @@ -116,6 +116,11 @@ public struct MapReport: Sendable { /// Number of online nodes (heard in the last 2 hours) this node has in its list that were received locally (not via MQTT) public var numOnlineLocalNodes: UInt32 = 0 + /// + /// User has opted in to share their location (map report) with the mqtt server + /// Controlled by map_report.should_report_location + public var hasOptedReportLocation_p: Bool = false + public var unknownFields = SwiftProtobuf.UnknownStorage() public init() {} @@ -189,6 +194,7 @@ extension MapReport: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementation 11: .same(proto: "altitude"), 12: .standard(proto: "position_precision"), 13: .standard(proto: "num_online_local_nodes"), + 14: .standard(proto: "has_opted_report_location"), ] public mutating func decodeMessage(decoder: inout D) throws { @@ -210,6 +216,7 @@ extension MapReport: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementation case 11: try { try decoder.decodeSingularInt32Field(value: &self.altitude) }() case 12: try { try decoder.decodeSingularUInt32Field(value: &self.positionPrecision) }() case 13: try { try decoder.decodeSingularUInt32Field(value: &self.numOnlineLocalNodes) }() + case 14: try { try decoder.decodeSingularBoolField(value: &self.hasOptedReportLocation_p) }() default: break } } @@ -255,6 +262,9 @@ extension MapReport: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementation if self.numOnlineLocalNodes != 0 { try visitor.visitSingularUInt32Field(value: self.numOnlineLocalNodes, fieldNumber: 13) } + if self.hasOptedReportLocation_p != false { + try visitor.visitSingularBoolField(value: self.hasOptedReportLocation_p, fieldNumber: 14) + } try unknownFields.traverse(visitor: &visitor) } @@ -272,6 +282,7 @@ extension MapReport: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementation if lhs.altitude != rhs.altitude {return false} if lhs.positionPrecision != rhs.positionPrecision {return false} if lhs.numOnlineLocalNodes != rhs.numOnlineLocalNodes {return false} + if lhs.hasOptedReportLocation_p != rhs.hasOptedReportLocation_p {return false} if lhs.unknownFields != rhs.unknownFields {return false} return true } diff --git a/MeshtasticProtobufs/Sources/meshtastic/telemetry.pb.swift b/MeshtasticProtobufs/Sources/meshtastic/telemetry.pb.swift index 90b56546..ccf4cfb4 100644 --- a/MeshtasticProtobufs/Sources/meshtastic/telemetry.pb.swift +++ b/MeshtasticProtobufs/Sources/meshtastic/telemetry.pb.swift @@ -176,6 +176,10 @@ public enum TelemetrySensorType: SwiftProtobuf.Enum, Swift.CaseIterable { /// /// RAKWireless RAK12035 Soil Moisture Sensor Module case rak12035 // = 37 + + /// + /// MAX17261 lipo battery gauge + case max17261 // = 38 case UNRECOGNIZED(Int) public init() { @@ -222,6 +226,7 @@ public enum TelemetrySensorType: SwiftProtobuf.Enum, Swift.CaseIterable { case 35: self = .dfrobotRain case 36: self = .dps310 case 37: self = .rak12035 + case 38: self = .max17261 default: self = .UNRECOGNIZED(rawValue) } } @@ -266,6 +271,7 @@ public enum TelemetrySensorType: SwiftProtobuf.Enum, Swift.CaseIterable { case .dfrobotRain: return 35 case .dps310: return 36 case .rak12035: return 37 + case .max17261: return 38 case .UNRECOGNIZED(let i): return i } } @@ -310,6 +316,7 @@ public enum TelemetrySensorType: SwiftProtobuf.Enum, Swift.CaseIterable { .dfrobotRain, .dps310, .rak12035, + .max17261, ] } @@ -1170,6 +1177,7 @@ extension TelemetrySensorType: SwiftProtobuf._ProtoNameProviding { 35: .same(proto: "DFROBOT_RAIN"), 36: .same(proto: "DPS310"), 37: .same(proto: "RAK12035"), + 38: .same(proto: "MAX17261"), ] } diff --git a/protobufs b/protobufs index 816595c8..47ec99aa 160000 --- a/protobufs +++ b/protobufs @@ -1 +1 @@ -Subproject commit 816595c8bbdfc3b4388e11348ccd043294d58705 +Subproject commit 47ec99aa4c4a2e3fff71fd5170663f0848deb021 From bd70589fe786ad624a9408f0de2685171226a33b Mon Sep 17 00:00:00 2001 From: Ben Meadors Date: Wed, 14 May 2025 16:40:02 -0500 Subject: [PATCH 039/213] Missed a couple --- Localizable.xcstrings | 4 ++-- Meshtastic/AppIntents/AddContactIntent.swift | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/Localizable.xcstrings b/Localizable.xcstrings index 23cb27cf..bff5fa52 100644 --- a/Localizable.xcstrings +++ b/Localizable.xcstrings @@ -15089,10 +15089,10 @@ } } }, - "Import Contact" : { + "Add Contact" : { }, - "Import Meshtastic Node %@ as a contact" : { + "Add Meshtastic Node %@ as a contact" : { }, "Import Route" : { diff --git a/Meshtastic/AppIntents/AddContactIntent.swift b/Meshtastic/AppIntents/AddContactIntent.swift index ff8ca149..17c6b960 100644 --- a/Meshtastic/AppIntents/AddContactIntent.swift +++ b/Meshtastic/AppIntents/AddContactIntent.swift @@ -9,10 +9,10 @@ import AppIntents import MeshtasticProtobufs struct AddContactIntent: AppIntent { - static var title: LocalizedStringResource = "Import Contact" + static var title: LocalizedStringResource = "Add Contact" static var description: IntentDescription = "Takes a Meshtastic contact URL and saves it to the nodes database" - @Parameter(title: "Contact URL", description: "The URL for the node to import") + @Parameter(title: "Contact URL", description: "The URL for the node to add") var contactUrl: URL // Define the function that performs the main logic @@ -33,7 +33,7 @@ struct AddContactIntent: AppIntent { do { let success = BLEManager.shared.addContactFromURL(base64UrlString: contactData) if !success { - throw AppIntentErrors.AppIntentError.message("Failed to import contact") + throw AppIntentErrors.AppIntentError.message("Failed to add contact") } } catch { From 7196596cae1fa46871e4d81e689d15cebc05a5ec Mon Sep 17 00:00:00 2001 From: Ben Meadors Date: Wed, 14 May 2025 16:59:57 -0500 Subject: [PATCH 040/213] Update Localizable.xcstrings Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- Localizable.xcstrings | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/Localizable.xcstrings b/Localizable.xcstrings index bff5fa52..0742d933 100644 --- a/Localizable.xcstrings +++ b/Localizable.xcstrings @@ -7203,7 +7203,14 @@ } }, "Contact URL" : { - + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Contact URL" + } + } + } }, "Contacts (%@)" : { "localizations" : { From 3d04610690ee229fa5240a6d1a496dd4db1abdaf Mon Sep 17 00:00:00 2001 From: Ben Meadors Date: Wed, 14 May 2025 19:35:29 -0500 Subject: [PATCH 041/213] Plumb proto --- Meshtastic/Extensions/CoreData/NodeInfoEntityToNodeInfo.swift | 1 + 1 file changed, 1 insertion(+) diff --git a/Meshtastic/Extensions/CoreData/NodeInfoEntityToNodeInfo.swift b/Meshtastic/Extensions/CoreData/NodeInfoEntityToNodeInfo.swift index 73706d2e..0c487346 100644 --- a/Meshtastic/Extensions/CoreData/NodeInfoEntityToNodeInfo.swift +++ b/Meshtastic/Extensions/CoreData/NodeInfoEntityToNodeInfo.swift @@ -15,6 +15,7 @@ extension NodeInfoEntity { userProto.shortName = user.shortName ?? "" userProto.hwModel = HardwareModel(rawValue: Int(user.hwModelId)) ?? .unset userProto.isLicensed = user.isLicensed + userProto.isUnmessagable = false userProto.role = Config.DeviceConfig.Role(rawValue: Int(user.role)) ?? .client userProto.publicKey = user.publicKey?.subdata(in: 0.. Date: Wed, 14 May 2025 20:15:50 -0700 Subject: [PATCH 042/213] Add new unmessagable field to core data --- Meshtastic.xcodeproj/project.pbxproj | 4 +- .../Meshtastic.xcdatamodeld/.xccurrentversion | 2 +- .../contents | 3 +- .../contents | 504 ++++++++++++++++++ Meshtastic/Persistence/UpdateCoreData.swift | 4 +- 5 files changed, 513 insertions(+), 4 deletions(-) create mode 100644 Meshtastic/Meshtastic.xcdatamodeld/MeshtasticDataModelV 51.xcdatamodel/contents diff --git a/Meshtastic.xcodeproj/project.pbxproj b/Meshtastic.xcodeproj/project.pbxproj index 66649df5..b2cf51ca 100644 --- a/Meshtastic.xcodeproj/project.pbxproj +++ b/Meshtastic.xcodeproj/project.pbxproj @@ -411,6 +411,7 @@ DD6193742862F6E600E59241 /* ExternalNotificationConfig.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExternalNotificationConfig.swift; sourceTree = ""; }; DD6193762862F90F00E59241 /* CannedMessagesConfig.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CannedMessagesConfig.swift; sourceTree = ""; }; DD6193782863875F00E59241 /* SerialConfig.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SerialConfig.swift; sourceTree = ""; }; + DD63CB4E2DD4FBEA00AFCAE2 /* MeshtasticDataModelV 51.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = "MeshtasticDataModelV 51.xcdatamodel"; sourceTree = ""; }; DD68BAE72C417A74004C01A0 /* MeshtasticDataModelV 40.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = "MeshtasticDataModelV 40.xcdatamodel"; sourceTree = ""; }; DD6D5A322CA1178300ED3032 /* TraceRoute.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TraceRoute.swift; sourceTree = ""; }; DD6D5A342CA13BA600ED3032 /* MeshtasticDataModelV 45.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = "MeshtasticDataModelV 45.xcdatamodel"; sourceTree = ""; }; @@ -1984,6 +1985,7 @@ DD3CC6BA28E366DF00FA9159 /* Meshtastic.xcdatamodeld */ = { isa = XCVersionGroup; children = ( + DD63CB4E2DD4FBEA00AFCAE2 /* MeshtasticDataModelV 51.xcdatamodel */, 233E99B32D84969500CC3A77 /* MeshtasticDataModelV 50.xcdatamodel */, 8D3F8A3D2D44B137009EAAA4 /* MeshtasticDataModelV 49.xcdatamodel */, DDA28B1B2D32C89200EF726F /* MeshtasticDataModelV 48.xcdatamodel */, @@ -2035,7 +2037,7 @@ DD5D0A9A2931AD6B00F7EA61 /* MeshtasticDataModelV2.xcdatamodel */, DD3CC6BB28E366DF00FA9159 /* MeshtasticDataModel.xcdatamodel */, ); - currentVersion = 233E99B32D84969500CC3A77 /* MeshtasticDataModelV 50.xcdatamodel */; + currentVersion = DD63CB4E2DD4FBEA00AFCAE2 /* MeshtasticDataModelV 51.xcdatamodel */; name = Meshtastic.xcdatamodeld; path = Meshtastic/Meshtastic.xcdatamodeld; sourceTree = ""; diff --git a/Meshtastic/Meshtastic.xcdatamodeld/.xccurrentversion b/Meshtastic/Meshtastic.xcdatamodeld/.xccurrentversion index cd530ab2..1853a00f 100644 --- a/Meshtastic/Meshtastic.xcdatamodeld/.xccurrentversion +++ b/Meshtastic/Meshtastic.xcdatamodeld/.xccurrentversion @@ -3,6 +3,6 @@ _XCCurrentVersionName - MeshtasticDataModelV 50.xcdatamodel + MeshtasticDataModelV 51.xcdatamodel diff --git a/Meshtastic/Meshtastic.xcdatamodeld/MeshtasticDataModelV 50.xcdatamodel/contents b/Meshtastic/Meshtastic.xcdatamodeld/MeshtasticDataModelV 50.xcdatamodel/contents index 9578507a..f9a2cddb 100644 --- a/Meshtastic/Meshtastic.xcdatamodeld/MeshtasticDataModelV 50.xcdatamodel/contents +++ b/Meshtastic/Meshtastic.xcdatamodeld/MeshtasticDataModelV 50.xcdatamodel/contents @@ -1,5 +1,5 @@ - + @@ -479,6 +479,7 @@ + diff --git a/Meshtastic/Meshtastic.xcdatamodeld/MeshtasticDataModelV 51.xcdatamodel/contents b/Meshtastic/Meshtastic.xcdatamodeld/MeshtasticDataModelV 51.xcdatamodel/contents new file mode 100644 index 00000000..9578507a --- /dev/null +++ b/Meshtastic/Meshtastic.xcdatamodeld/MeshtasticDataModelV 51.xcdatamodel/contents @@ -0,0 +1,504 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/Meshtastic/Persistence/UpdateCoreData.swift b/Meshtastic/Persistence/UpdateCoreData.swift index 00916edb..5e271228 100644 --- a/Meshtastic/Persistence/UpdateCoreData.swift +++ b/Meshtastic/Persistence/UpdateCoreData.swift @@ -160,7 +160,9 @@ func upsertNodeInfoPacket (packet: MeshPacket, context: NSManagedObjectContext) newNode.channel = Int32(packet.channel) } if let nodeInfoMessage = try? NodeInfo(serializedBytes: packet.decoded.payload) { - newNode.hopsAway = Int32(nodeInfoMessage.hopsAway) + if nodeInfoMessage.hasHopsAway { + newNode.hopsAway = Int32(nodeInfoMessage.hopsAway) + } newNode.favorite = nodeInfoMessage.isFavorite } From 690b41e0a06a7aa3fbe9cea8cb18b94545f02459 Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Thu, 15 May 2025 07:44:12 -0700 Subject: [PATCH 043/213] Implement unmessagable --- Localizable.xcstrings | 14 +++++------ .../contents | 1 - .../contents | 3 ++- Meshtastic/Persistence/UpdateCoreData.swift | 22 +++++++++++++++++ Meshtastic/Views/Messages/UserList.swift | 24 ++++++++----------- 5 files changed, 41 insertions(+), 23 deletions(-) diff --git a/Localizable.xcstrings b/Localizable.xcstrings index 0742d933..27f582c3 100644 --- a/Localizable.xcstrings +++ b/Localizable.xcstrings @@ -2136,6 +2136,12 @@ } } } + }, + "Add Contact" : { + + }, + "Add Meshtastic Node %@ as a contact" : { + }, "Add to favorites" : { "localizations" : { @@ -15095,12 +15101,6 @@ } } } - }, - "Add Contact" : { - - }, - "Add Meshtastic Node %@ as a contact" : { - }, "Import Route" : { "localizations" : { @@ -30920,7 +30920,7 @@ } } }, - "The URL for the node to import" : { + "The URL for the node to add" : { }, "There has been no response to a request for device metadata over the admin channel for this node." : { diff --git a/Meshtastic/Meshtastic.xcdatamodeld/MeshtasticDataModelV 50.xcdatamodel/contents b/Meshtastic/Meshtastic.xcdatamodeld/MeshtasticDataModelV 50.xcdatamodel/contents index f9a2cddb..1124c59b 100644 --- a/Meshtastic/Meshtastic.xcdatamodeld/MeshtasticDataModelV 50.xcdatamodel/contents +++ b/Meshtastic/Meshtastic.xcdatamodeld/MeshtasticDataModelV 50.xcdatamodel/contents @@ -479,7 +479,6 @@ - diff --git a/Meshtastic/Meshtastic.xcdatamodeld/MeshtasticDataModelV 51.xcdatamodel/contents b/Meshtastic/Meshtastic.xcdatamodeld/MeshtasticDataModelV 51.xcdatamodel/contents index 9578507a..f9a2cddb 100644 --- a/Meshtastic/Meshtastic.xcdatamodeld/MeshtasticDataModelV 51.xcdatamodel/contents +++ b/Meshtastic/Meshtastic.xcdatamodeld/MeshtasticDataModelV 51.xcdatamodel/contents @@ -1,5 +1,5 @@ - + @@ -479,6 +479,7 @@ + diff --git a/Meshtastic/Persistence/UpdateCoreData.swift b/Meshtastic/Persistence/UpdateCoreData.swift index 5e271228..d7d25a9d 100644 --- a/Meshtastic/Persistence/UpdateCoreData.swift +++ b/Meshtastic/Persistence/UpdateCoreData.swift @@ -183,6 +183,17 @@ func upsertNodeInfoPacket (packet: MeshPacket, context: NSManagedObjectContext) newUser.role = Int32(newUserMessage.role.rawValue) newUser.hwModel = String(describing: newUserMessage.hwModel).uppercased() newUser.hwModelId = Int32(newUserMessage.hwModel.rawValue) + if newUserMessage.hasIsUnmessagable { + newUser.unmessagable = newUserMessage.isUnmessagable + } else { + // For older firmare make Repeater, Router, Router Late, Sensor, Tracker, TAK, and TAK Tracker unmessagable + let roles: [Int32] = [4, 2, 11, 6, 7, 10] + if roles.contains(newUser.role) { + newUser.unmessagable = true + } else { + newUser.unmessagable = false + } + } if !newUserMessage.publicKey.isEmpty { newUser.pkiEncrypted = true newUser.publicKey = newUserMessage.publicKey @@ -279,6 +290,17 @@ func upsertNodeInfoPacket (packet: MeshPacket, context: NSManagedObjectContext) fetchedNode[0].user!.role = Int32(nodeInfoMessage.user.role.rawValue) fetchedNode[0].user!.hwModel = String(describing: nodeInfoMessage.user.hwModel).uppercased() fetchedNode[0].user!.hwModelId = Int32(nodeInfoMessage.user.hwModel.rawValue) + if nodeInfoMessage.user.hasIsUnmessagable { + fetchedNode[0].user!.unmessagable = nodeInfoMessage.user.isUnmessagable + } else { + // For older firmare make Repeater, Router, Router Late, Sensor, Tracker, TAK, and TAK Tracker unmessagable + let roles: [Int32] = [-1, 4, 2, 11, 6, 7, 10] + if roles.contains(fetchedNode[0].user?.role ?? -1) { + fetchedNode[0].user!.unmessagable = true + } else { + fetchedNode[0].user!.unmessagable = false + } + } if !nodeInfoMessage.user.publicKey.isEmpty { fetchedNode[0].user!.pkiEncrypted = true fetchedNode[0].user!.publicKey = nodeInfoMessage.user.publicKey diff --git a/Meshtastic/Views/Messages/UserList.swift b/Meshtastic/Views/Messages/UserList.swift index 7e57ce8c..ddb1264e 100644 --- a/Meshtastic/Views/Messages/UserList.swift +++ b/Meshtastic/Views/Messages/UserList.swift @@ -46,9 +46,8 @@ struct UserList: View { NSSortDescriptor(key: "userNode.lastHeard", ascending: false), NSSortDescriptor(key: "longName", ascending: true)], predicate: NSPredicate( - format: "userNode.ignored == false && longName != '' AND NOT (userNode.viaMqtt == YES AND userNode.hopsAway > 0)" - ), animation: .default - ) + format: "userNode.ignored == false && longName != '' AND unmessagable == false" + ), animation: .default) var users: FetchedResults @Binding var node: NodeInfoEntity? @@ -298,17 +297,19 @@ struct UserList: View { let textSearchPredicate = NSCompoundPredicate(type: .or, subpredicates: searchPredicates) /// Create an array of predicates to hold our AND predicates var predicates: [NSPredicate] = [] + let defaultPredicate = NSPredicate(format: "userNode.ignored == NO AND longName != '' AND unmessagable == NO") + predicates.append(defaultPredicate) /// Mqtt and lora if !(viaLora && viaMqtt) { if viaLora { let loraPredicate = NSPredicate(format: "userNode.viaMqtt == NO") predicates.append(loraPredicate) } else { - let mqttPredicate = NSPredicate(format: "userNode.viaMqtt == YES AND userNode.hopsAway == 0") + let mqttPredicate = NSPredicate(format: "userNode.viaMqtt == YES") predicates.append(mqttPredicate) } } else { - let mqttPredicate = NSPredicate(format: "NOT (userNode.viaMqtt == YES AND userNode.hopsAway > 0)") + let mqttPredicate = NSPredicate(format: "NOT (userNode.viaMqtt == YES)") predicates.append(mqttPredicate) } /// Roles @@ -362,16 +363,11 @@ struct UserList: View { predicates.append(distancePredicate) } } - - if predicates.count > 0 || !searchText.isEmpty { - if !searchText.isEmpty { - let filterPredicates = NSCompoundPredicate(type: .and, subpredicates: predicates) - users.nsPredicate = NSCompoundPredicate(type: .and, subpredicates: [textSearchPredicate, filterPredicates]) - } else { - users.nsPredicate = NSCompoundPredicate(type: .and, subpredicates: predicates) - } + if !searchText.isEmpty { + let filterPredicates = NSCompoundPredicate(type: .and, subpredicates: predicates) + users.nsPredicate = NSCompoundPredicate(type: .and, subpredicates: [textSearchPredicate, filterPredicates]) } else { - users.nsPredicate = nil + users.nsPredicate = NSCompoundPredicate(type: .and, subpredicates: predicates) } } } From faa6886ec2d5ae858fdc005c36e13c63661d29fd Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Thu, 15 May 2025 08:06:37 -0700 Subject: [PATCH 044/213] Add unmessagable to node list --- Meshtastic/Views/Nodes/NodeList.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Meshtastic/Views/Nodes/NodeList.swift b/Meshtastic/Views/Nodes/NodeList.swift index 370f03eb..86bdabac 100644 --- a/Meshtastic/Views/Nodes/NodeList.swift +++ b/Meshtastic/Views/Nodes/NodeList.swift @@ -92,7 +92,7 @@ struct NodeList: View { FavoriteNodeButton(bleManager: bleManager, context: context, node: node) /// Don't show message, trace route, position exchange or delete context menu items for the connected node if connectedNode.num != node.num { - if !node.viaMqtt || node.viaMqtt && node.hopsAway == 0 { + if !(node.user?.unmessagable ?? true) { Button(action: { if let url = URL(string: "meshtastic:///messages?userNum=\(node.num)") { UIApplication.shared.open(url) From f59a945a8d146df158b57fe4b70ec37f4e0fa84b Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Thu, 15 May 2025 18:59:38 -0700 Subject: [PATCH 045/213] Set unmessagable more places --- Meshtastic/Helpers/MeshPackets.swift | 12 ++++++++++++ Meshtastic/Persistence/UpdateCoreData.swift | 8 ++++---- 2 files changed, 16 insertions(+), 4 deletions(-) diff --git a/Meshtastic/Helpers/MeshPackets.swift b/Meshtastic/Helpers/MeshPackets.swift index cb8608bd..e0870401 100644 --- a/Meshtastic/Helpers/MeshPackets.swift +++ b/Meshtastic/Helpers/MeshPackets.swift @@ -317,6 +317,12 @@ func nodeInfoPacket (nodeInfo: NodeInfo, channel: UInt32, context: NSManagedObje newUser.pkiEncrypted = true newUser.publicKey = nodeInfo.user.publicKey } + let roles: [Int32] = [2, 4, 5, 6, 7, 10, 11] + if roles.contains(Int32(newUser.role)) { + newUser.unmessagable = true + } else { + newUser.unmessagable = false + } newNode.user = newUser } else if nodeInfo.num > Constants.minimumNodeNum { let newUser = createUser(num: Int64(nodeInfo.num), context: context) @@ -389,6 +395,12 @@ func nodeInfoPacket (nodeInfo: NodeInfo, channel: UInt32, context: NSManagedObje fetchedNode[0].user!.role = Int32(nodeInfo.user.role.rawValue) fetchedNode[0].user!.hwModel = String(describing: nodeInfo.user.hwModel).uppercased() fetchedNode[0].user!.hwModelId = Int32(nodeInfo.user.hwModel.rawValue) + let roles: [Int32] = [-1, 2, 4, 5, 6, 7, 10, 11] + if roles.contains(Int32(fetchedNode[0].user?.role ?? -1)) { + fetchedNode[0].user!.unmessagable = true + } else { + fetchedNode[0].user!.unmessagable = false + } Task { Api().loadDeviceHardwareData { (hw) in let dh = hw.first(where: { $0.hwModel == fetchedNode[0].user!.hwModelId }) diff --git a/Meshtastic/Persistence/UpdateCoreData.swift b/Meshtastic/Persistence/UpdateCoreData.swift index d7d25a9d..2daa8d2b 100644 --- a/Meshtastic/Persistence/UpdateCoreData.swift +++ b/Meshtastic/Persistence/UpdateCoreData.swift @@ -187,8 +187,8 @@ func upsertNodeInfoPacket (packet: MeshPacket, context: NSManagedObjectContext) newUser.unmessagable = newUserMessage.isUnmessagable } else { // For older firmare make Repeater, Router, Router Late, Sensor, Tracker, TAK, and TAK Tracker unmessagable - let roles: [Int32] = [4, 2, 11, 6, 7, 10] - if roles.contains(newUser.role) { + let roles: [Int32] = [2, 4, 5, 6, 7, 10, 11] + if roles.contains(Int32(newUser.role)) { newUser.unmessagable = true } else { newUser.unmessagable = false @@ -294,8 +294,8 @@ func upsertNodeInfoPacket (packet: MeshPacket, context: NSManagedObjectContext) fetchedNode[0].user!.unmessagable = nodeInfoMessage.user.isUnmessagable } else { // For older firmare make Repeater, Router, Router Late, Sensor, Tracker, TAK, and TAK Tracker unmessagable - let roles: [Int32] = [-1, 4, 2, 11, 6, 7, 10] - if roles.contains(fetchedNode[0].user?.role ?? -1) { + let roles: [Int32] = [-1, 2, 4, 5, 6, 7, 10, 11] + if roles.contains(Int32(fetchedNode[0].user?.role ?? -1)) { fetchedNode[0].user!.unmessagable = true } else { fetchedNode[0].user!.unmessagable = false From b0f1dbf355e0060e50ba2c3d9735593cc542e350 Mon Sep 17 00:00:00 2001 From: gitbisector Date: Sun, 11 May 2025 16:12:34 -0700 Subject: [PATCH 046/213] Additional accessibilityLabels for VoiceOver users (take #3) --- Localizable.xcstrings | 480 +++++++++++++++++- Meshtastic/Extensions/String.swift | 11 + .../Helpers/BLESignalStrengthIndicator.swift | 81 +-- Meshtastic/Views/Helpers/BatteryCompact.swift | 115 +++-- Meshtastic/Views/Helpers/BatteryGauge.swift | 35 +- .../Views/Helpers/ConnectedDevice.swift | 50 +- .../RequestPositionButton.swift | 1 + .../TextMessageField/TextMessageSize.swift | 2 + .../Helpers/Actions/IgnoreNodeButton.swift | 1 + .../Views/Nodes/Helpers/NodeDetail.swift | 159 +++--- .../Views/Nodes/Helpers/NodeInfoItem.swift | 4 + .../Views/Nodes/Helpers/NodeListItem.swift | 95 +++- Meshtastic/Views/Nodes/NodeList.swift | 5 + 13 files changed, 875 insertions(+), 164 deletions(-) diff --git a/Localizable.xcstrings b/Localizable.xcstrings index aa5cbd4c..3a479de4 100644 --- a/Localizable.xcstrings +++ b/Localizable.xcstrings @@ -35342,7 +35342,485 @@ } } } + }, + "ble.signal.strength.weak" : { + "comment" : "VoiceOver value for weak BLE signal strength", + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "needs_review", + "value" : "Signalstärke schwach" + } + }, + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Signal strength weak" + } + }, + "it" : { + "stringUnit" : { + "state" : "needs_review", + "value" : "Segnale debole" + } + }, + "sr" : { + "stringUnit" : { + "state" : "needs_review", + "value" : "Слаб сигнал" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "needs_review", + "value" : "信号弱" + } + }, + "zh-Hant-TW" : { + "stringUnit" : { + "state" : "needs_review", + "value" : "訊號微弱" + } + } + } + }, + "signal_strength" : { + "comment" : "VoiceOver label for signal strength indicator", + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "needs_review", + "value" : "Signalstärke" + } + }, + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Signal strength" + } + }, + "it" : { + "stringUnit" : { + "state" : "needs_review", + "value" : "Intensità del segnale" + } + }, + "sr" : { + "stringUnit" : { + "state" : "needs_review", + "value" : "Јачина сигнала" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "needs_review", + "value" : "信号强度" + } + }, + "zh-Hant-TW" : { + "stringUnit" : { + "state" : "needs_review", + "value" : "訊號強度" + } + } + } + }, + "message_size" : { + "comment" : "VoiceOver label for message size", + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "needs_review", + "value" : "Nachrichtengröße" + } + }, + "en" : { + "stringUnit" : { + "state" : "needs_review", + "value" : "Message size" + } + }, + "it" : { + "stringUnit" : { + "state" : "needs_review", + "value" : "Dimensione messaggio" + } + }, + "sr" : { + "stringUnit" : { + "state" : "needs_review", + "value" : "Величина поруке" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "needs_review", + "value" : "消息大小" + } + }, + "zh-Hant-TW" : { + "stringUnit" : { + "state" : "needs_review", + "value" : "訊息大小" + } + } + } + }, + "device_charging" : { + "comment" : "VoiceOver value for charging device", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Charging" + } + } + } + }, + "Bluetooth is off.off" : { + "extractionState" : "stale", + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Bluetooth ist aus" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Le Bluetooth est arrêté" + } + }, + "he" : { + "stringUnit" : { + "state" : "translated", + "value" : "בלוטוס כבוי" + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Il Bluetooth è spento" + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Bluetooth jest wyłączony" + } + }, + "se" : { + "stringUnit" : { + "state" : "translated", + "value" : "Bluetooth är avstängt" + } + }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Блутут је искључен" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "蓝牙已关闭" + } + }, + "zh-Hant-TW" : { + "stringUnit" : { + "state" : "translated", + "value" : "藍芽已關閉" + } + } + } + }, + "bytes_used" : { + "comment" : "VoiceOver value for bytes used", + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "needs_review", + "value" : "%d von %d Bytes verwendet" + } + }, + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "%d of %d bytes used" + } + }, + "it" : { + "stringUnit" : { + "state" : "needs_review", + "value" : "%d di %d byte usati" + } + }, + "sr" : { + "stringUnit" : { + "state" : "needs_review", + "value" : "%d од %d бајтова искоришћено" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "needs_review", + "value" : "已用%d/%d字节" + } + }, + "zh-Hant-TW" : { + "stringUnit" : { + "state" : "needs_review", + "value" : "已用%d/%d位元組" + } + } + } + }, + "heading" : { + "comment" : "Heading label for VoiceOver" + }, + "Hide sidebar" : {}, + "bluetooth.not.connected" : { + "comment" : "VoiceOver label for disconnected Bluetooth icon", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "No Bluetooth device connected" + } + } + } + }, + "device.configuration" : { + "extractionState" : "stale", + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "needs_review", + "value" : "Gerätekonfiguration" + } + }, + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Device Configuration" + } + }, + "he" : { + "stringUnit" : { + "state" : "needs_review", + "value" : "Device Configuration" + } + }, + "it" : { + "stringUnit" : { + "state" : "needs_review", + "value" : "Configurazione del dispositivo" + } + }, + "pl" : { + "stringUnit" : { + "state" : "needs_review", + "value" : "Device Configuration" + } + }, + "se" : { + "stringUnit" : { + "state" : "needs_review", + "value" : "Enhetsinställningar" + } + }, + "sr" : { + "stringUnit" : { + "state" : "needs_review", + "value" : "Подешавања уређаја" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "needs_review", + "value" : "设备配置" + } + }, + "zh-Hant-TW" : { + "stringUnit" : { + "state" : "needs_review", + "value" : "設備設定" + } + } + } + }, + "ble.signal.strength.strong" : { + "comment" : "VoiceOver value for strong BLE signal strength", + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "needs_review", + "value" : "Signalstärke stark" + } + }, + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Signal strength strong" + } + }, + "it" : { + "stringUnit" : { + "state" : "needs_review", + "value" : "Segnale forte" + } + }, + "sr" : { + "stringUnit" : { + "state" : "needs_review", + "value" : "Јак сигнал" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "needs_review", + "value" : "信号强" + } + }, + "zh-Hant-TW" : { + "stringUnit" : { + "state" : "needs_review", + "value" : "訊號強" + } + } + } + }, + "ble.signal.strength.normal" : { + "comment" : "VoiceOver value for normal BLE signal strength", + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "needs_review", + "value" : "Signalstärke normal" + } + }, + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Signal strength normal" + } + }, + "it" : { + "stringUnit" : { + "state" : "needs_review", + "value" : "Segnale normale" + } + }, + "sr" : { + "stringUnit" : { + "state" : "needs_review", + "value" : "Нормалан сигнал" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "needs_review", + "value" : "信号正常" + } + }, + "zh-Hant-TW" : { + "stringUnit" : { + "state" : "needs_review", + "value" : "訊號正常" + } + } + } + }, + "bluetooth.connected" : { + "comment" : "VoiceOver label for connected Bluetooth icon", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Connected to Bluetooth device" + } + } + } + }, + "request_position" : { + "comment" : "VoiceOver label for request position button", + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Position anfordern" + } + }, + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Request position" + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Richiedi posizione" + } + }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Захтевај позицију" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "请求位置" + } + }, + "zh-Hant-TW" : { + "stringUnit" : { + "state" : "translated", + "value" : "請求位置" + } + } + } + }, + "distance" : { + "comment" : "Distance label for VoiceOver" + }, + "device_plugged_in" : { + "comment" : "VoiceOver value for plugged in device", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Plugged in" + } + } + } + }, + "unknown" : { + "localizations" : { + "it" : { + "stringUnit" : { + "state" : "needs_review", + "value" : "sconosciuto" + } + }, + "sr" : { + "stringUnit" : { + "state" : "needs_review", + "value" : "непознато" + } + }, + "zh-Hant-TW" : { + "stringUnit" : { + "state" : "needs_review", + "value" : "未知" + } + } + } } }, "version" : "1.0" -} \ No newline at end of file +} diff --git a/Meshtastic/Extensions/String.swift b/Meshtastic/Extensions/String.swift index d2ae1e5a..6a57da9e 100644 --- a/Meshtastic/Extensions/String.swift +++ b/Meshtastic/Extensions/String.swift @@ -115,6 +115,17 @@ extension String { .joined() } + /// Formats a short name like "P130" to read as "Node P 130" for VoiceOver + /// This ensures proper pronunciation of alphanumeric node IDs + func formatNodeNameForVoiceOver() -> String { + let spaced = self.replacingOccurrences( + of: #"([A-Za-z])([0-9]+)"#, + with: "$1 $2", + options: .regularExpression + ) + return "Node " + spaced + } + // Adds variation selectors to prefer the graphical form of emoji. // Looks ahead to make sure that the variation selector is not already applied. var addingVariationSelectors: String { diff --git a/Meshtastic/Views/Helpers/BLESignalStrengthIndicator.swift b/Meshtastic/Views/Helpers/BLESignalStrengthIndicator.swift index c5d17f16..6b0287b0 100644 --- a/Meshtastic/Views/Helpers/BLESignalStrengthIndicator.swift +++ b/Meshtastic/Views/Helpers/BLESignalStrengthIndicator.swift @@ -32,47 +32,64 @@ import Foundation import SwiftUI struct SignalStrengthIndicator: View { - let signalStrength: BLESignalStrength + // Accessibility: VoiceOver description + private var accessibilityDescription: String { + switch signalStrength { + case .weak: + return NSLocalizedString("ble.signal.strength.weak", comment: "VoiceOver value for weak BLE signal strength") + case .normal: + return NSLocalizedString("ble.signal.strength.normal", comment: "VoiceOver value for normal BLE signal strength") + case .strong: + return NSLocalizedString("ble.signal.strength.strong", comment: "VoiceOver value for strong BLE signal strength") + } + } - var body: some View { - HStack { - ForEach(0..<3) { bar in - RoundedRectangle(cornerRadius: 3) - .divided(amount: (CGFloat(bar) + 1) / CGFloat(3)) - .fill(getColor().opacity(bar <= signalStrength.rawValue ? 1 : 0.3)) - .frame(width: 8, height: 40) - } - } - } + let signalStrength: BLESignalStrength - private func getColor() -> Color { - switch signalStrength { - case .weak: - return Color.red - case .normal: - return Color.yellow - case .strong: - return Color.green - } - } + var body: some View { + Group { + HStack { + ForEach(0..<3) { bar in + RoundedRectangle(cornerRadius: 3) + .divided(amount: (CGFloat(bar) + 1) / CGFloat(3)) + .fill(getColor().opacity(bar <= signalStrength.rawValue ? 1 : 0.3)) + .frame(width: 8, height: 40) + } + } + } + .accessibilityElement(children: .ignore) + .accessibilityLabel(NSLocalizedString("signal_strength", comment: "VoiceOver label for signal strength indicator")) + .accessibilityValue(accessibilityDescription) + } + + private func getColor() -> Color { + switch signalStrength { + case .weak: + return Color.red + case .normal: + return Color.yellow + case .strong: + return Color.green + } + } } struct Divided: Shape { - var amount: CGFloat // Should be in range 0...1 - var shape: S - func path(in rect: CGRect) -> Path { - shape.path(in: rect.divided(atDistance: amount * rect.height, from: .maxYEdge).slice) - } + var amount: CGFloat // Should be in range 0...1 + var shape: S + func path(in rect: CGRect) -> Path { + shape.path(in: rect.divided(atDistance: amount * rect.height, from: .maxYEdge).slice) + } } extension Shape { - func divided(amount: CGFloat) -> Divided { - return Divided(amount: amount, shape: self) - } + func divided(amount: CGFloat) -> Divided { + return Divided(amount: amount, shape: self) + } } enum BLESignalStrength: Int { - case weak = 0 - case normal = 1 - case strong = 2 + case weak = 0 + case normal = 1 + case strong = 2 } diff --git a/Meshtastic/Views/Helpers/BatteryCompact.swift b/Meshtastic/Views/Helpers/BatteryCompact.swift index 4ac61d0c..bb9819a2 100644 --- a/Meshtastic/Views/Helpers/BatteryCompact.swift +++ b/Meshtastic/Views/Helpers/BatteryCompact.swift @@ -13,69 +13,104 @@ struct BatteryCompact: View { var color: Color var body: some View { + // Group the battery icon and label in a single accessible container HStack(alignment: .center, spacing: 0) { if let batteryLevel { - if batteryLevel == 100 { - Image(systemName: "battery.100.bolt") - .font(iconFont) - .foregroundColor(color) - .symbolRenderingMode(.multicolor) - } else if batteryLevel < 100 && batteryLevel > 74 { - Image(systemName: "battery.75") - .font(iconFont) - .foregroundColor(color) - .symbolRenderingMode(.multicolor) - } else if batteryLevel < 75 && batteryLevel > 49 { - Image(systemName: "battery.50") - .font(iconFont) - .foregroundColor(color) - .symbolRenderingMode(.multicolor) - } else if batteryLevel < 50 && batteryLevel > 14 { - Image(systemName: "battery.25") - .font(iconFont) - .foregroundColor(color) - .symbolRenderingMode(.multicolor) - } else if batteryLevel < 15 && batteryLevel > 0 { - Image(systemName: "battery.0") - .font(iconFont) - .foregroundColor(color) - .symbolRenderingMode(.multicolor) - } else if batteryLevel == 0 { - Image(systemName: "battery.0") - .font(iconFont) - .foregroundColor(.red) - .symbolRenderingMode(.multicolor) - } else if batteryLevel > 100 { + // Check for plugged in state + let isPluggedIn = batteryLevel > 100 + let isCharging = batteryLevel == 100 + + // Battery icon selection based on level + if isPluggedIn { Image(systemName: "powerplug") .font(iconFont) .foregroundColor(color) .symbolRenderingMode(.multicolor) + .accessibilityHidden(true) // Hide from VoiceOver since container will handle it + } else if isCharging { + Image(systemName: "battery.100.bolt") + .font(iconFont) + .foregroundColor(color) + .symbolRenderingMode(.multicolor) + .accessibilityHidden(true) + } else if batteryLevel > 74 { + Image(systemName: "battery.75") + .font(iconFont) + .foregroundColor(color) + .symbolRenderingMode(.multicolor) + .accessibilityHidden(true) + } else if batteryLevel > 49 { + Image(systemName: "battery.50") + .font(iconFont) + .foregroundColor(color) + .symbolRenderingMode(.multicolor) + .accessibilityHidden(true) + } else if batteryLevel > 14 { + Image(systemName: "battery.25") + .font(iconFont) + .foregroundColor(color) + .symbolRenderingMode(.multicolor) + .accessibilityHidden(true) + } else if batteryLevel > 0 { + Image(systemName: "battery.0") + .font(iconFont) + .foregroundColor(color) + .symbolRenderingMode(.multicolor) + .accessibilityHidden(true) + } else { + Image(systemName: "battery.0") + .font(iconFont) + .foregroundColor(.red) + .symbolRenderingMode(.multicolor) + .accessibilityHidden(true) } - } else { - Image(systemName: "battery.0") - .font(iconFont) - .foregroundColor(color) - .symbolRenderingMode(.multicolor) - } - if let batteryLevel { - if batteryLevel > 100 { + + // Battery text label + if isPluggedIn { Text("PWD") .foregroundStyle(.secondary) .font(font) - } else if batteryLevel == 100 { + .accessibilityHidden(true) + } else if isCharging { Text("CHG") .foregroundStyle(.secondary) .font(font) + .accessibilityHidden(true) } else { Text(verbatim: "\(batteryLevel.formatted(.number.precision(.fractionLength(0))))%") .foregroundStyle(.secondary) .font(font) + .accessibilityHidden(true) } } else { + // Unknown battery state + Image(systemName: "battery.0") + .font(iconFont) + .foregroundColor(color) + .symbolRenderingMode(.multicolor) + .accessibilityHidden(true) + Text(verbatim: "?") .foregroundStyle(.secondary) .font(font) + .accessibilityHidden(true) } } + // Setup container-level accessibility for VoiceOver + .accessibilityElement(children: .ignore) + .accessibilityLabel(NSLocalizedString("Battery Level", comment: "VoiceOver label for battery gauge")) + // Set appropriate value based on the battery state using a computed property + .accessibilityValue(batteryLevel.map { level in + if level > 100 { + // Plugged in - same as PWD visual indicator + return NSLocalizedString("device_plugged_in", comment: "VoiceOver value for plugged in device") + } else if level == 100 { + // Charging - same as CHG visual indicator + return NSLocalizedString("device_charging", comment: "VoiceOver value for charging device") + } else { + // Normal battery level + return String(format: NSLocalizedString("battery_level_percent", comment: "VoiceOver value for battery level"), Int(level)) + } + } ?? "Unknown") } } diff --git a/Meshtastic/Views/Helpers/BatteryGauge.swift b/Meshtastic/Views/Helpers/BatteryGauge.swift index 952c9768..81e81e7e 100644 --- a/Meshtastic/Views/Helpers/BatteryGauge.swift +++ b/Meshtastic/Views/Helpers/BatteryGauge.swift @@ -18,18 +18,20 @@ struct BatteryGauge: View { let deviceMetrics = node.telemetries?.filtered(using: NSPredicate(format: "metricsType == 0")) let mostRecent = deviceMetrics?.lastObject as? TelemetryEntity - let batteryLevel = Double(mostRecent?.batteryLevel ?? 0) + // For VoiceOver purposes, detect when device is plugged in (battery > 100%) + let isPluggedIn = (mostRecent?.batteryLevel ?? 0) > 100 + // Use a capped battery level for UI display + let batteryLevel = Double(min(100, mostRecent?.batteryLevel ?? 0)) VStack { - if batteryLevel > 100.0 { - // Plugged in - Image(systemName: "powerplug") - .font(.largeTitle) - .foregroundColor(.accentColor) - .symbolRenderingMode(.hierarchical) + if isPluggedIn { + // Use a completely standalone view for the plugged in state + // to avoid any VoiceOver confusion + PluggedInIndicator() } else { let gradient = Gradient(colors: [.red, .orange, .green]) Gauge(value: batteryLevel, in: minValue...maxValue) { + // Accessibility for battery gauge if batteryLevel >= 0.0 && batteryLevel < 10 { Label("Battery Level %", systemImage: "battery.0") } else if batteryLevel >= 10.0 && batteryLevel < 25.00 { @@ -50,6 +52,8 @@ struct BatteryGauge: View { Text(Int(batteryLevel), format: .percent) } } + .accessibilityLabel(NSLocalizedString("Battery Level", comment: "VoiceOver label for battery gauge")) + .accessibilityValue(String(format: NSLocalizedString("Battery Level %", comment: "VoiceOver value for battery level"), Int(batteryLevel))) .tint(gradient) .gaugeStyle(.accessoryCircular) } @@ -63,6 +67,23 @@ struct BatteryGauge: View { } } +/// A dedicated view for showing a device is plugged in +/// With proper VoiceOver support that matches the visual indication +struct PluggedInIndicator: View { + var body: some View { + // This view is isolated from any battery measurement + // to ensure VoiceOver doesn't pick up any percentages + Image(systemName: "powerplug") + .font(.largeTitle) + .foregroundColor(.accentColor) + .symbolRenderingMode(.hierarchical) + // Override the accessibility to ensure correct VoiceOver announcement + .accessibilityElement(children: .ignore) + .accessibilityLabel(NSLocalizedString("Battery Level", comment: "VoiceOver label for battery gauge")) + .accessibilityValue(NSLocalizedString("device_plugged_in", comment: "VoiceOver value for plugged in device")) + } +} + struct BatteryGauge_Previews: PreviewProvider { static var previews: some View { VStack { diff --git a/Meshtastic/Views/Helpers/ConnectedDevice.swift b/Meshtastic/Views/Helpers/ConnectedDevice.swift index c795b1b0..4a46db41 100644 --- a/Meshtastic/Views/Helpers/ConnectedDevice.swift +++ b/Meshtastic/Views/Helpers/ConnectedDevice.swift @@ -21,22 +21,46 @@ struct ConnectedDevice: View { if (phoneOnly && UIDevice.current.userInterfaceIdiom == .phone) || !phoneOnly { if bluetoothOn { if deviceConnected { - if mqttUplinkEnabled || mqttDownlinkEnabled { - MQTTIcon(connected: mqttProxyConnected, uplink: mqttUplinkEnabled, downlink: mqttDownlinkEnabled, topic: mqttTopic) - } - Image(systemName: "antenna.radiowaves.left.and.right.circle.fill") - .imageScale(.large) - .foregroundColor(.green) - .symbolRenderingMode(.hierarchical) - Text(name.addingVariationSelectors).font(name.isEmoji() ? .title : .callout).foregroundColor(.gray) + // Create an HStack for connected state with proper accessibility + HStack { + if mqttUplinkEnabled || mqttDownlinkEnabled { + MQTTIcon(connected: mqttProxyConnected, uplink: mqttUplinkEnabled, downlink: mqttDownlinkEnabled, topic: mqttTopic) + .accessibilityHidden(true) + } + Image(systemName: "antenna.radiowaves.left.and.right.circle.fill") + .imageScale(.large) + .foregroundColor(.green) + .symbolRenderingMode(.hierarchical) + .accessibilityHidden(true) + Text(name.addingVariationSelectors) + .font(name.isEmoji() ? .title : .callout) + .foregroundColor(.gray) + .accessibilityHidden(true) + } + .accessibilityElement(children: .ignore) + .accessibilityLabel("bluetooth.connected".localized + ", " + name.formatNodeNameForVoiceOver()) } else { - Image(systemName: "antenna.radiowaves.left.and.right.slash") - .imageScale(.medium) - .foregroundColor(.red) - .symbolRenderingMode(.hierarchical) + // Create a container for disconnected state + HStack { + Image(systemName: "antenna.radiowaves.left.and.right.slash") + .imageScale(.medium) + .foregroundColor(.red) + .symbolRenderingMode(.hierarchical) + .accessibilityHidden(true) + } + .accessibilityElement(children: .ignore) + .accessibilityLabel("bluetooth.not.connected".localized) } } else { - Text("Bluetooth is off").font(.subheadline).foregroundColor(.red) + // Create a container for Bluetooth off state + HStack { + Text("bluetooth.off".localized) + .font(.subheadline) + .foregroundColor(.red) + .accessibilityHidden(true) + } + .accessibilityElement(children: .ignore) + .accessibilityLabel("bluetooth.off".localized) } } } diff --git a/Meshtastic/Views/Messages/TextMessageField/RequestPositionButton.swift b/Meshtastic/Views/Messages/TextMessageField/RequestPositionButton.swift index 2f1634bc..fd166f51 100644 --- a/Meshtastic/Views/Messages/TextMessageField/RequestPositionButton.swift +++ b/Meshtastic/Views/Messages/TextMessageField/RequestPositionButton.swift @@ -6,6 +6,7 @@ struct RequestPositionButton: View { var body: some View { Button(action: action) { Image(systemName: "mappin.and.ellipse") + .accessibilityLabel(NSLocalizedString("request_position", comment: "VoiceOver label for request position button")) .symbolRenderingMode(.hierarchical) .imageScale(.large) .foregroundColor(.accentColor) diff --git a/Meshtastic/Views/Messages/TextMessageField/TextMessageSize.swift b/Meshtastic/Views/Messages/TextMessageField/TextMessageSize.swift index aacbd60d..9839e246 100644 --- a/Meshtastic/Views/Messages/TextMessageField/TextMessageSize.swift +++ b/Meshtastic/Views/Messages/TextMessageField/TextMessageSize.swift @@ -6,6 +6,8 @@ struct TextMessageSize: View { var body: some View { ProgressView("\("Bytes".localized): \(totalBytes) / \(maxbytes)", value: Double(totalBytes), total: Double(maxbytes)) + .accessibilityLabel(NSLocalizedString("message_size", comment: "VoiceOver label for message size")) + .accessibilityValue(String(format: NSLocalizedString("bytes_used", comment: "VoiceOver value for bytes used"), totalBytes, maxbytes)) .frame(width: 130) .padding(5) .font(.subheadline) diff --git a/Meshtastic/Views/Nodes/Helpers/Actions/IgnoreNodeButton.swift b/Meshtastic/Views/Nodes/Helpers/Actions/IgnoreNodeButton.swift index 84fdf4d3..2d73d5c0 100644 --- a/Meshtastic/Views/Nodes/Helpers/Actions/IgnoreNodeButton.swift +++ b/Meshtastic/Views/Nodes/Helpers/Actions/IgnoreNodeButton.swift @@ -40,6 +40,7 @@ struct IgnoreNodeButton: View { Image(systemName: node.ignored ? "minus.circle.fill" : "minus.circle") .symbolRenderingMode(.multicolor) } + // Accessibility: Label for VoiceOver } } } diff --git a/Meshtastic/Views/Nodes/Helpers/NodeDetail.swift b/Meshtastic/Views/Nodes/Helpers/NodeDetail.swift index 081e7adc..c5670e06 100644 --- a/Meshtastic/Views/Nodes/Helpers/NodeDetail.swift +++ b/Meshtastic/Views/Nodes/Helpers/NodeDetail.swift @@ -46,7 +46,8 @@ struct NodeDetail: View { Section("Hardware") { NodeInfoItem(node: node) } - Section("Node") { + .accessibilityElement(children: .combine) + Section("Node") { // Node HStack(alignment: .center) { Spacer() CircleText( @@ -67,6 +68,7 @@ struct NodeDetail: View { .foregroundColor(getRssiColor(rssi: node.rssi)) .font(.caption) } + .accessibilityElement(children: .combine) } if node.telemetries?.count ?? 0 > 0 { Spacer() @@ -74,6 +76,7 @@ struct NodeDetail: View { } Spacer() } + .accessibilityElement(children: .combine) .listRowSeparator(.hidden) if let user = node.user { if !user.keyMatch { @@ -86,6 +89,7 @@ struct NodeDetail: View { .foregroundStyle(.secondary) .font(.callout) } + .accessibilityElement(children: .combine) } icon: { Image(systemName: "key.slash.fill") .symbolRenderingMode(.multicolor) @@ -104,6 +108,7 @@ struct NodeDetail: View { Text(String(node.num)) .textSelection(.enabled) } + .accessibilityElement(children: .combine) HStack { Label { @@ -116,6 +121,7 @@ struct NodeDetail: View { Text(node.num.toHex()) .textSelection(.enabled) } + .accessibilityElement(children: .combine) if let metadata = node.metadata { HStack { @@ -129,6 +135,7 @@ struct NodeDetail: View { Text(metadata.firmwareVersion ?? "Unknown".localized) } + .accessibilityElement(children: .combine) } if let role = node.user?.role, let deviceRole = DeviceRoles(rawValue: Int(role)) { @@ -142,6 +149,7 @@ struct NodeDetail: View { Spacer() Text(deviceRole.name) } + .accessibilityElement(children: .combine) } if let dm = node.telemetries?.filtered(using: NSPredicate(format: "metricsType == 0")).lastObject as? TelemetryEntity, let uptimeSeconds = dm.uptimeSeconds { @@ -161,6 +169,7 @@ struct NodeDetail: View { Text(uptime) .textSelection(.enabled) } + .accessibilityElement(children: .combine) } if let firstHeard = node.firstHeard, firstHeard.timeIntervalSince1970 > 0 && firstHeard < Calendar.current.date(byAdding: .year, value: 1, to: Date())! { @@ -179,7 +188,9 @@ struct NodeDetail: View { Text(firstHeard.formatted()) .textSelection(.enabled) } - }.onTapGesture { + } + .accessibilityElement(children: .combine) + .onTapGesture { dateFormatRelative.toggle() } } @@ -203,7 +214,9 @@ struct NodeDetail: View { Text(lastHeard.formatted()) .textSelection(.enabled) } - }.onTapGesture { + } + .accessibilityElement(children: .combine) + .onTapGesture { dateFormatRelative.toggle() } } @@ -216,79 +229,84 @@ struct NodeDetail: View { if node.hasPositions && UserDefaults.environmentEnableWeatherKit || node.hasDataForLatestEnvironmentMetrics(attributes: ["iaq", "temperature", "relativeHumidity", "barometricPressure", "windSpeed", "radiation", "weight", "Distance", "soilTemperature", "soilMoisture"]) { Section("Environment") { - if !node.hasEnvironmentMetrics { - LocalWeatherConditions(location: node.latestPosition?.nodeLocation) - } else { - VStack { - if node.latestEnvironmentMetrics?.iaq ?? -1 > 0 { - IndoorAirQuality(iaq: Int(node.latestEnvironmentMetrics?.iaq ?? 0), displayMode: .gradient) - .padding(.vertical) - } - LazyVGrid(columns: gridItemLayout) { - if let temperature = node.latestEnvironmentMetrics?.temperature?.shortFormattedTemperature() { - WeatherConditionsCompactWidget(temperature: String(temperature), symbolName: "cloud.sun", description: "TEMP") + // Group weather/environment data for better VoiceOver experience + VStack { + if !node.hasEnvironmentMetrics { + LocalWeatherConditions(location: node.latestPosition?.nodeLocation) + } else { + VStack { + if node.latestEnvironmentMetrics?.iaq ?? -1 > 0 { + IndoorAirQuality(iaq: Int(node.latestEnvironmentMetrics?.iaq ?? 0), displayMode: .gradient) + .padding(.vertical) } - if let humidity = node.latestEnvironmentMetrics?.relativeHumidity { - if let temperature = node.latestEnvironmentMetrics?.temperature { - let dewPoint = calculateDewPoint(temp: temperature, relativeHumidity: humidity) - .formatted(.number.precision(.fractionLength(0))) + "°" - HumidityCompactWidget(humidity: Int(humidity), dewPoint: dewPoint) - } else { - HumidityCompactWidget(humidity: Int(humidity), dewPoint: nil) + LazyVGrid(columns: gridItemLayout) { + if let temperature = node.latestEnvironmentMetrics?.temperature?.shortFormattedTemperature() { + WeatherConditionsCompactWidget(temperature: String(temperature), symbolName: "cloud.sun", description: "TEMP") + } + if let humidity = node.latestEnvironmentMetrics?.relativeHumidity { + if let temperature = node.latestEnvironmentMetrics?.temperature { + let dewPoint = calculateDewPoint(temp: temperature, relativeHumidity: humidity) + .formatted(.number.precision(.fractionLength(0))) + "°" + HumidityCompactWidget(humidity: Int(humidity), dewPoint: dewPoint) + } else { + HumidityCompactWidget(humidity: Int(humidity), dewPoint: nil) + } + } + if let pressure = node.latestEnvironmentMetrics?.barometricPressure { + PressureCompactWidget(pressure: pressure.formatted(.number.precision(.fractionLength(2))), unit: "hPA", low: pressure <= 1009.144) + } + if let windSpeed = node.latestEnvironmentMetrics?.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)) + 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) + } + if let rainfall1h = node.latestEnvironmentMetrics?.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 + let unitLabel = usesMetricSystem ? "mm" : "in" + let measurement = Measurement(value: Double(rainfall1h), unit: UnitLength.millimeters) + let decimals = usesMetricSystem ? 0 : 1 + 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 { + 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 + let unitLabel = usesMetricSystem ? "mm" : "in" + let measurement = Measurement(value: Double(rainfall24h), unit: UnitLength.millimeters) + let decimals = usesMetricSystem ? 0 : 1 + 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 { + RadiationCompactWidget(radiation: radiation.formatted(.number.precision(.fractionLength(1))), unit: "µR/hr") + } + if let weight = node.latestEnvironmentMetrics?.weight { + WeightCompactWidget(weight: weight.formatted(.number.precision(.fractionLength(1))), unit: "kg") + } + if let distance = node.latestEnvironmentMetrics?.distance { + DistanceCompactWidget(distance: distance.formatted(.number.precision(.fractionLength(0))), unit: "mm") + } + if let soilTemperature = node.latestEnvironmentMetrics?.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 { + SoilMoistureCompactWidget(moisture: soilMoisture.formatted(.number.precision(.fractionLength(0))), unit: "%") } } - if let pressure = node.latestEnvironmentMetrics?.barometricPressure { - PressureCompactWidget(pressure: pressure.formatted(.number.precision(.fractionLength(2))), unit: "hPA", low: pressure <= 1009.144) - } - if let windSpeed = node.latestEnvironmentMetrics?.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)) - 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) - } - if let rainfall1h = node.latestEnvironmentMetrics?.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 - let unitLabel = usesMetricSystem ? "mm" : "in" - let measurement = Measurement(value: Double(rainfall1h), unit: UnitLength.millimeters) - let decimals = usesMetricSystem ? 0 : 1 - 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 { - 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 - let unitLabel = usesMetricSystem ? "mm" : "in" - let measurement = Measurement(value: Double(rainfall24h), unit: UnitLength.millimeters) - let decimals = usesMetricSystem ? 0 : 1 - 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 { - RadiationCompactWidget(radiation: radiation.formatted(.number.precision(.fractionLength(1))), unit: "µR/hr") - } - if let weight = node.latestEnvironmentMetrics?.weight { - WeightCompactWidget(weight: weight.formatted(.number.precision(.fractionLength(1))), unit: "kg") - } - if let distance = node.latestEnvironmentMetrics?.distance { - DistanceCompactWidget(distance: distance.formatted(.number.precision(.fractionLength(0))), unit: "mm") - } - if let soilTemperature = node.latestEnvironmentMetrics?.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 { - SoilMoistureCompactWidget(moisture: soilMoisture.formatted(.number.precision(.fractionLength(0))), unit: "%") - } + .padding(node.latestEnvironmentMetrics?.iaq ?? -1 > 0 ? .bottom : .vertical) } - .padding(node.latestEnvironmentMetrics?.iaq ?? -1 > 0 ? .bottom : .vertical) } } + // Apply accessibility properties to the environment section + .accessibilityElement(children: .combine) } } if node.hasPowerMetrics && node.latestPowerMetrics != nil { @@ -298,6 +316,7 @@ struct NodeDetail: View { PowerMetrics(metric: metric) } } + .accessibilityElement(children: .combine) } } Section("Logs") { diff --git a/Meshtastic/Views/Nodes/Helpers/NodeInfoItem.swift b/Meshtastic/Views/Nodes/Helpers/NodeInfoItem.swift index 07f3d92c..eb4c37b0 100644 --- a/Meshtastic/Views/Nodes/Helpers/NodeInfoItem.swift +++ b/Meshtastic/Views/Nodes/Helpers/NodeInfoItem.swift @@ -31,6 +31,7 @@ struct NodeInfoItem: View { .foregroundStyle(.gray) .font(.callout) } + .accessibilityElement(children: .combine) Spacer() } VStack(alignment: .center) { @@ -49,9 +50,11 @@ struct NodeInfoItem: View { .cornerRadius(5) } } + .accessibilityElement(children: .combine) } Spacer() } + .accessibilityElement(children: .combine) .onAppear { Api().loadDeviceHardwareData { (hw) in for device in hw { @@ -79,6 +82,7 @@ struct NodeInfoItem: View { Text(String("incomplete".localized)) } } + .accessibilityElement(children: .combine) } } } diff --git a/Meshtastic/Views/Nodes/Helpers/NodeListItem.swift b/Meshtastic/Views/Nodes/Helpers/NodeListItem.swift index 2978ceab..62dd5fd0 100644 --- a/Meshtastic/Views/Nodes/Helpers/NodeListItem.swift +++ b/Meshtastic/Views/Nodes/Helpers/NodeListItem.swift @@ -7,9 +7,99 @@ import SwiftUI import CoreLocation +import Foundation struct NodeListItem: View { + // Accessibility: Synthesized description for VoiceOver + private var accessibilityDescription: String { + var desc = "" + if let shortName = node.user?.shortName { + // Format the shortName using the String extension method + desc = shortName.formatNodeNameForVoiceOver() + } else if let longName = node.user?.longName { + desc = longName + } else { + desc = "unknown node" + } + if connected { + desc += ", currently connected" + } + if node.favorite { + desc += ", favorite" + } + if node.lastHeard != nil { + let formatter = RelativeDateTimeFormatter() + formatter.unitsStyle = .full + let relative = formatter.localizedString(for: node.lastHeard!, relativeTo: Date()) + desc += ", last heard " + relative + } + if node.isOnline { + desc += ", online" + } else { + desc += ", offline" + } + let role = DeviceRoles(rawValue: Int(node.user?.role ?? 0)) + if let roleName = role?.name { + desc += ", role: \(roleName)" + } + if node.hopsAway > 0 { + desc += ", \(node.hopsAway) hops away" + } + if let battery = node.latestDeviceMetrics?.batteryLevel { + // Check for plugged in and charging states, same logic as in BatteryCompact and BatteryGauge + if battery > 100 { + desc += ", " + NSLocalizedString("device_plugged_in", comment: "VoiceOver value for plugged in device") + } else if battery == 100 { + desc += ", " + NSLocalizedString("device_charging", comment: "VoiceOver value for charging device") + } else { + desc += ", battery \(battery)%" + } + } + // Add distance and heading/bearing if available, but only for non-connected nodes + if !connected, let (lastPosition, myCoord) = locationData { + let nodeCoord = CLLocation(latitude: lastPosition.nodeCoordinate!.latitude, longitude: lastPosition.nodeCoordinate!.longitude) + let metersAway = nodeCoord.distance(from: myCoord) + + // Distance information + let distanceFormatter = LengthFormatter() + distanceFormatter.unitStyle = .medium + let formattedDistance = distanceFormatter.string(fromMeters: metersAway) + // For VoiceOver, prepend 'Distance' (localized) + desc += ", " + String(format: "%@: %@", NSLocalizedString("distance", comment: "Distance label for VoiceOver"), formattedDistance) + + // Add bearing/heading information for VoiceOver + let trueBearing = getBearingBetweenTwoPoints(point1: myCoord, point2: nodeCoord) + let heading = Measurement(value: trueBearing, unit: UnitAngle.degrees) + let formattedHeading = heading.formatted(.measurement(width: .narrow, numberFormatStyle: .number.precision(.fractionLength(0)))) + // Using a direct format without requiring a new localization key + desc += ", " + NSLocalizedString("heading", comment: "Heading label for VoiceOver") + " " + formattedHeading + } + // Add signal strength if available + if node.snr != 0 && !node.viaMqtt { + let signalStrength: BLESignalStrength + if node.snr < -10 { + signalStrength = .weak + } else if node.snr < 5 { + signalStrength = .normal + } else { + signalStrength = .strong + } + let signalString: String + switch signalStrength { + case .weak: + signalString = NSLocalizedString("ble.signal.strength.weak", comment: "VoiceOver value for weak BLE signal strength") + case .normal: + signalString = NSLocalizedString("ble.signal.strength.normal", comment: "VoiceOver value for normal BLE signal strength") + case .strong: + signalString = NSLocalizedString("ble.signal.strength.strong", comment: "VoiceOver value for strong BLE signal strength") + } + desc += ", " + signalString + } + return desc + } + + @ObservedObject var node: NodeInfoEntity var connected: Bool var connectedNode: Int64 @@ -167,7 +257,10 @@ struct NodeListItem: View { } .padding(.top, 4) .padding(.bottom, 4) - } + // Accessibility: Make the whole row a single element for VoiceOver + .accessibilityElement(children: .ignore) + .accessibilityLabel(accessibilityDescription) + } } struct DefaultIcon: View { diff --git a/Meshtastic/Views/Nodes/NodeList.swift b/Meshtastic/Views/Nodes/NodeList.swift index 1e823020..6b305148 100644 --- a/Meshtastic/Views/Nodes/NodeList.swift +++ b/Meshtastic/Views/Nodes/NodeList.swift @@ -243,6 +243,8 @@ struct NodeList: View { phoneOnly: true ) } + // Make sure the ZStack passes through accessibility to the ConnectedDevice component + .accessibilityElement(children: .contain) ) } content: { if let node = selectedNode { @@ -261,6 +263,7 @@ struct NodeList: View { } label: { Image(systemName: "rectangle") } + .accessibilityLabel("Hide sidebar") } ConnectedDevice( bluetoothOn: bleManager.isSwitchedOn, @@ -269,6 +272,8 @@ struct NodeList: View { phoneOnly: true ) } + // Make sure the ZStack passes through accessibility to the ConnectedDevice component + .accessibilityElement(children: .contain) ) } } else { From 20922d70c068613e215a24f349b903b72a057344 Mon Sep 17 00:00:00 2001 From: unojazz Date: Mon, 19 May 2025 21:08:23 -0400 Subject: [PATCH 047/213] translation update --- Localizable.xcstrings | 153 ++++++++++++++++++++++++++++++++++++++---- 1 file changed, 140 insertions(+), 13 deletions(-) diff --git a/Localizable.xcstrings b/Localizable.xcstrings index 27f582c3..d1865493 100644 --- a/Localizable.xcstrings +++ b/Localizable.xcstrings @@ -2138,10 +2138,24 @@ } }, "Add Contact" : { - + "localizations" : { + "zh-Hant-TW" : { + "stringUnit" : { + "state" : "translated", + "value" : "增加聯絡人" + } + } + } }, "Add Meshtastic Node %@ as a contact" : { - + "localizations" : { + "zh-Hant-TW" : { + "stringUnit" : { + "state" : "translated", + "value" : "將 Meshtastic 節點 %@ 新增為聯絡人" + } + } + } }, "Add to favorites" : { "localizations" : { @@ -2290,7 +2304,14 @@ } }, "Administration Enabled" : { - + "localizations" : { + "zh-Hant-TW" : { + "stringUnit" : { + "state" : "translated", + "value" : "管理功能已啟用" + } + } + } }, "Advanced" : { "localizations" : { @@ -7215,6 +7236,12 @@ "state" : "translated", "value" : "Contact URL" } + }, + "zh-Hant-TW" : { + "stringUnit" : { + "state" : "translated", + "value" : "聯絡人網址" + } } } }, @@ -10012,7 +10039,14 @@ } }, "Done" : { - + "localizations" : { + "zh-Hant-TW" : { + "stringUnit" : { + "state" : "translated", + "value" : "完成" + } + } + } }, "Double Tap as Button" : { "localizations" : { @@ -17393,7 +17427,14 @@ } }, "message" : { - + "localizations" : { + "zh-Hant-TW" : { + "stringUnit" : { + "state" : "translated", + "value" : "訊息" + } + } + } }, "Message" : { "localizations" : { @@ -21636,6 +21677,12 @@ "state" : "translated", "value" : "Конфигурација PAX бројача примљена: %@" } + }, + "zh-Hant-TW" : { + "stringUnit" : { + "state" : "translated", + "value" : "PAX 計數器設定已收到: %@" + } } } }, @@ -21720,7 +21767,14 @@ } }, "paxcounter.log" : { - + "localizations" : { + "zh-Hant-TW" : { + "stringUnit" : { + "state" : "translated", + "value" : "paxcounter.log" + } + } + } }, "Perform a factory reset on the node you are connected to" : { "localizations" : { @@ -22255,6 +22309,12 @@ "state" : "translated", "value" : "Конфигурација позиције примљена: %@" } + }, + "zh-Hant-TW" : { + "stringUnit" : { + "state" : "translated", + "value" : "收到位置設定檔: %@" + } } } }, @@ -24517,7 +24577,14 @@ } }, "Replying to a message" : { - + "localizations" : { + "zh-Hant-TW" : { + "stringUnit" : { + "state" : "translated", + "value" : "正在回覆訊息" + } + } + } }, "Request Legacy Admin: %@" : { "localizations" : { @@ -26042,7 +26109,14 @@ } }, "Scan this QR code to add %@ to another device." : { - + "localizations" : { + "zh-Hant-TW" : { + "stringUnit" : { + "state" : "translated", + "value" : "掃描這個QR code 以便將 %@ 增加到另一個裝置。" + } + } + } }, "Screen on for" : { "localizations" : { @@ -26441,7 +26515,14 @@ } }, "Select a node from the drop down to manage connected or remote devices." : { - + "localizations" : { + "zh-Hant-TW" : { + "stringUnit" : { + "state" : "translated", + "value" : "從下拉選單中選擇一個節點,以管理已連接或遠端的裝置。" + } + } + } }, "Select a Trace Route" : { "localizations" : { @@ -28057,7 +28138,14 @@ } }, "Share Contact QR" : { - + "localizations" : { + "zh-Hant-TW" : { + "stringUnit" : { + "state" : "translated", + "value" : "分享聯絡人 QR 碼" + } + } + } }, "Share QR Code" : { "localizations" : { @@ -29550,6 +29638,12 @@ "state" : "translated", "value" : "Подсистем" } + }, + "zh-Hant-TW" : { + "stringUnit" : { + "state" : "translated", + "value" : "子系統" + } } } }, @@ -29738,7 +29832,14 @@ } }, "Takes a Meshtastic contact URL and saves it to the nodes database" : { - + "localizations" : { + "zh-Hant-TW" : { + "stringUnit" : { + "state" : "translated", + "value" : "將 Meshtastic 聯絡人的網址儲存到節點資料庫中。" + } + } + } }, "Tapback" : { "localizations" : { @@ -30921,7 +31022,14 @@ } }, "The URL for the node to add" : { - + "localizations" : { + "zh-Hant-TW" : { + "stringUnit" : { + "state" : "translated", + "value" : "要新增的節點網址" + } + } + } }, "There has been no response to a request for device metadata over the admin channel for this node." : { "localizations" : { @@ -31950,7 +32058,14 @@ } }, "Trace Route (in %@s)" : { - + "localizations" : { + "zh-Hant-TW" : { + "stringUnit" : { + "state" : "translated", + "value" : "追蹤路由(在 %@ 秒)" + } + } + } }, "Trace Route Log" : { "localizations" : { @@ -32133,6 +32248,12 @@ "state" : "translated", "value" : "追踪器" } + }, + "zh-Hant-TW" : { + "stringUnit" : { + "state" : "translated", + "value" : "追蹤器" + } } } }, @@ -33913,6 +34034,12 @@ "state" : "new", "value" : "Version: %1$@ (%2$@)" } + }, + "zh-Hant-TW" : { + "stringUnit" : { + "state" : "translated", + "value" : "版本: %1$@ (%2$@)" + } } } }, From f80c477d2bb42c3cd581e38ff3d1532ec5553382 Mon Sep 17 00:00:00 2001 From: Ben Meadors Date: Tue, 20 May 2025 11:30:53 -0500 Subject: [PATCH 048/213] upsertNodeInfoPacket for contacts --- Meshtastic/Helpers/BLEManager.swift | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/Meshtastic/Helpers/BLEManager.swift b/Meshtastic/Helpers/BLEManager.swift index cb759ffb..d769393d 100644 --- a/Meshtastic/Helpers/BLEManager.swift +++ b/Meshtastic/Helpers/BLEManager.swift @@ -1799,9 +1799,26 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate guard let binaryData: Data = try? toRadio.serializedData() else { return false } + + // Create a NodeInfo (User) packet for the newly added contact + var dataNodeMessage = DataMessage() + if let nodeInfoData = try? contact.user.serializedData() { + dataNodeMessage.payload = nodeInfoData + dataNodeMessage.portnum = PortNum.nodeinfoApp + + var nodeMeshPacket = MeshPacket() + nodeMeshPacket.id = UInt32.random(in: UInt32(UInt8.max).. Date: Tue, 20 May 2025 11:59:37 -0500 Subject: [PATCH 049/213] Fix add channel URL scanning --- Meshtastic/MeshtasticApp.swift | 120 +++++++++++++++++---------------- 1 file changed, 63 insertions(+), 57 deletions(-) diff --git a/Meshtastic/MeshtasticApp.swift b/Meshtastic/MeshtasticApp.swift index 1a4ba85b..9e763aed 100644 --- a/Meshtastic/MeshtasticApp.swift +++ b/Meshtastic/MeshtasticApp.swift @@ -60,8 +60,11 @@ struct MeshtasticAppleApp: App { .onContinueUserActivity(NSUserActivityTypeBrowsingWeb) { userActivity in Logger.mesh.debug("URL received \(userActivity, privacy: .public)") self.incomingUrl = userActivity.webpageURL - - if (self.incomingUrl?.absoluteString.lowercased().contains("meshtastic.org/e/#")) != nil { + self.saveChannels = false + if (self.incomingUrl?.absoluteString.lowercased().contains("meshtastic.org/v/#") == true) { + handleContactUrl(url: self.incomingUrl!) + } + 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 if (self.incomingUrl?.absoluteString.lowercased().contains("?")) != nil { @@ -85,64 +88,10 @@ struct MeshtasticAppleApp: App { } } .onOpenURL(perform: { (url) in - Logger.mesh.debug("Some sort of URL was received \(url, privacy: .public)") self.incomingUrl = url if url.absoluteString.lowercased().contains("meshtastic.org/v/#") { - let components = self.incomingUrl?.absoluteString.components(separatedBy: "#") ?? [] - // Extract contact information from the URL - if let contactData = components.last { - - let decodedString = contactData.base64urlToBase64() - if let decodedData = Data(base64Encoded: decodedString) { - do { - let contact = try MeshtasticProtobufs.SharedContact(serializedBytes: decodedData) - - // Show an alert to confirm adding the contact - let alertController = UIAlertController( - title: "Add Contact", - message: "Would you like to add \(contact.user.longName) as a contact?", - preferredStyle: .alert - ) - - alertController.addAction(UIAlertAction( - title: "Yes", - style: .default, - handler: { _ in - let success = BLEManager.shared.addContactFromURL(base64UrlString: contactData) - Logger.services.debug("Contact added from URL: \(success ? "success" : "failed")") - } - )) - - alertController.addAction(UIAlertAction( - title: "No", - style: .cancel, - handler: nil - )) - - // Present the alert - if let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene, - let rootViewController = windowScene.windows.first?.rootViewController { - rootViewController.present(alertController, animated: true) - } - Logger.services.debug("Contact data extracted from URL: \(contactData, privacy: .public)") - } catch { - Logger.services.error("Failed to parse contact data: \(error.localizedDescription, privacy: .public)") - - // Show error alert to user - if let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene, - let rootViewController = windowScene.windows.first?.rootViewController { - let errorAlert = UIAlertController( - title: "Error", - message: "Could not process contact information. Invalid format.", - preferredStyle: .alert - ) - errorAlert.addAction(UIAlertAction(title: "OK", style: .default)) - rootViewController.present(errorAlert, animated: true) - } - } - } - } + handleContactUrl(url: url) } 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 @@ -199,4 +148,61 @@ struct MeshtasticAppleApp: App { } } } + + func handleContactUrl(url: URL) { + let components = self.incomingUrl?.absoluteString.components(separatedBy: "#") ?? [] + // Extract contact information from the URL + if let contactData = components.last { + + let decodedString = contactData.base64urlToBase64() + if let decodedData = Data(base64Encoded: decodedString) { + do { + let contact = try MeshtasticProtobufs.SharedContact(serializedBytes: decodedData) + + // Show an alert to confirm adding the contact + let alertController = UIAlertController( + title: "Add Contact", + message: "Would you like to add \(contact.user.longName) as a contact?", + preferredStyle: .alert + ) + + alertController.addAction(UIAlertAction( + title: "Yes", + style: .default, + handler: { _ in + let success = BLEManager.shared.addContactFromURL(base64UrlString: contactData) + Logger.services.debug("Contact added from URL: \(success ? "success" : "failed")") + } + )) + + alertController.addAction(UIAlertAction( + title: "No", + style: .cancel, + handler: nil + )) + + // Present the alert + if let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene, + let rootViewController = windowScene.windows.first?.rootViewController { + rootViewController.present(alertController, animated: true) + } + Logger.services.debug("Contact data extracted from URL: \(contactData, privacy: .public)") + } catch { + Logger.services.error("Failed to parse contact data: \(error.localizedDescription, privacy: .public)") + + // Show error alert to user + if let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene, + let rootViewController = windowScene.windows.first?.rootViewController { + let errorAlert = UIAlertController( + title: "Error", + message: "Could not process contact information. Invalid format.", + preferredStyle: .alert + ) + errorAlert.addAction(UIAlertAction(title: "OK", style: .default)) + rootViewController.present(errorAlert, animated: true) + } + } + } + } + } } From a2c869ec067cd12c2d0a436f92d0afc75652f552 Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Tue, 20 May 2025 21:16:12 -0700 Subject: [PATCH 050/213] Fix localization keys merged from community pull requests --- Localizable.xcstrings | 896 ++++-------------- Meshtastic/Enums/RouteEnums.swift | 12 +- Meshtastic/Extensions/String.swift | 2 +- .../Helpers/BLESignalStrengthIndicator.swift | 2 +- Meshtastic/Views/Helpers/BatteryCompact.swift | 4 +- .../RequestPositionButton.swift | 2 +- .../TextMessageField/TextMessageSize.swift | 4 +- .../Views/Nodes/Helpers/NodeListItem.swift | 8 +- 8 files changed, 218 insertions(+), 712 deletions(-) diff --git a/Localizable.xcstrings b/Localizable.xcstrings index b183b5ec..f08588ee 100644 --- a/Localizable.xcstrings +++ b/Localizable.xcstrings @@ -4070,6 +4070,7 @@ } }, "Battery Level" : { + "comment" : "VoiceOver label for battery gauge", "localizations" : { "de" : { "stringUnit" : { @@ -4128,6 +4129,7 @@ } }, "Battery Level %" : { + "comment" : "VoiceOver value for battery level", "localizations" : { "it" : { "stringUnit" : { @@ -4183,40 +4185,6 @@ } } }, - "biking" : { - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "biken" - } - }, - "it" : { - "stringUnit" : { - "state" : "translated", - "value" : "tour in bicicletta" - } - }, - "sr" : { - "stringUnit" : { - "state" : "translated", - "value" : "тура бициклом" - } - }, - "zh-Hans" : { - "stringUnit" : { - "state" : "translated", - "value" : "自行车旅行" - } - }, - "zh-Hant-TW" : { - "stringUnit" : { - "state" : "translated", - "value" : "自行車" - } - } - } - }, "Biking" : { "localizations" : { "de" : { @@ -4451,6 +4419,129 @@ } } }, + "ble.signal.strength.normal" : { + "comment" : "VoiceOver value for normal BLE signal strength", + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "needs_review", + "value" : "Signalstärke normal" + } + }, + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Signal strength normal" + } + }, + "it" : { + "stringUnit" : { + "state" : "needs_review", + "value" : "Segnale normale" + } + }, + "sr" : { + "stringUnit" : { + "state" : "needs_review", + "value" : "Нормалан сигнал" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "needs_review", + "value" : "信号正常" + } + }, + "zh-Hant-TW" : { + "stringUnit" : { + "state" : "needs_review", + "value" : "訊號正常" + } + } + } + }, + "ble.signal.strength.strong" : { + "comment" : "VoiceOver value for strong BLE signal strength", + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "needs_review", + "value" : "Signalstärke stark" + } + }, + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Signal strength strong" + } + }, + "it" : { + "stringUnit" : { + "state" : "needs_review", + "value" : "Segnale forte" + } + }, + "sr" : { + "stringUnit" : { + "state" : "needs_review", + "value" : "Јак сигнал" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "needs_review", + "value" : "信号强" + } + }, + "zh-Hant-TW" : { + "stringUnit" : { + "state" : "needs_review", + "value" : "訊號強" + } + } + } + }, + "ble.signal.strength.weak" : { + "comment" : "VoiceOver value for weak BLE signal strength", + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "needs_review", + "value" : "Signalstärke schwach" + } + }, + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Signal strength weak" + } + }, + "it" : { + "stringUnit" : { + "state" : "needs_review", + "value" : "Segnale debole" + } + }, + "sr" : { + "stringUnit" : { + "state" : "needs_review", + "value" : "Слаб сигнал" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "needs_review", + "value" : "信号弱" + } + }, + "zh-Hant-TW" : { + "stringUnit" : { + "state" : "needs_review", + "value" : "訊號微弱" + } + } + } + }, "Bluetooth" : { "localizations" : { "de" : { @@ -4683,6 +4774,28 @@ } } }, + "bluetooth.connected" : { + "comment" : "VoiceOver label for connected Bluetooth icon", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Connected to Bluetooth device" + } + } + } + }, + "bluetooth.not.connected" : { + "comment" : "VoiceOver label for disconnected Bluetooth icon", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "No Bluetooth device connected" + } + } + } + }, "Broadcast Interval" : { "localizations" : { "it" : { @@ -5037,6 +5150,9 @@ } } }, + "Bytes Used" : { + "comment" : "VoiceOver value for bytes used" + }, "Call Sign" : { "localizations" : { "it" : { @@ -7231,15 +7347,9 @@ }, "Contact URL" : { "localizations" : { - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Contact URL" - } - }, "zh-Hant-TW" : { "stringUnit" : { - "state" : "translated", + "state" : "needs_review", "value" : "聯絡人網址" } } @@ -9352,6 +9462,28 @@ } } }, + "device_charging" : { + "comment" : "VoiceOver value for charging device", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Charging" + } + } + } + }, + "device_plugged_in" : { + "comment" : "VoiceOver value for plugged in device", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Plugged in" + } + } + } + }, "Dilution of precision (DOP) PDOP used by default" : { "localizations" : { "it" : { @@ -10252,40 +10384,6 @@ } } }, - "driving" : { - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "fahren" - } - }, - "it" : { - "stringUnit" : { - "state" : "translated", - "value" : "guida" - } - }, - "sr" : { - "stringUnit" : { - "state" : "translated", - "value" : "вожња" - } - }, - "zh-Hans" : { - "stringUnit" : { - "state" : "translated", - "value" : "驾驶" - } - }, - "zh-Hant-TW" : { - "stringUnit" : { - "state" : "translated", - "value" : "開車" - } - } - } - }, "Driving" : { "localizations" : { "de" : { @@ -14053,6 +14151,9 @@ } } } + }, + "Hide sidebar" : { + }, "HIGH" : { "localizations" : { @@ -14088,40 +14189,6 @@ } } }, - "hiking" : { - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "wandern" - } - }, - "it" : { - "stringUnit" : { - "state" : "translated", - "value" : "escursione" - } - }, - "sr" : { - "stringUnit" : { - "state" : "translated", - "value" : "планинарње" - } - }, - "zh-Hans" : { - "stringUnit" : { - "state" : "translated", - "value" : "徒步" - } - }, - "zh-Hant-TW" : { - "stringUnit" : { - "state" : "translated", - "value" : "登山" - } - } - } - }, "Hiking" : { "localizations" : { "de" : { @@ -17432,16 +17499,6 @@ } } }, - "message" : { - "localizations" : { - "zh-Hant-TW" : { - "stringUnit" : { - "state" : "translated", - "value" : "訊息" - } - } - } - }, "Message" : { "localizations" : { "de" : { @@ -17672,6 +17729,9 @@ } } }, + "Message Size" : { + "comment" : "VoiceOver label for message size" + }, "Message Status Options" : { "localizations" : { "it" : { @@ -21326,34 +21386,6 @@ } } }, - "overlanding" : { - "localizations" : { - "it" : { - "stringUnit" : { - "state" : "translated", - "value" : "overland drive" - } - }, - "sr" : { - "stringUnit" : { - "state" : "translated", - "value" : "Вожња преко копна" - } - }, - "zh-Hans" : { - "stringUnit" : { - "state" : "translated", - "value" : "越野" - } - }, - "zh-Hant-TW" : { - "stringUnit" : { - "state" : "translated", - "value" : "開車 (overland drive)" - } - } - } - }, "Overlanding" : { "localizations" : { "it" : { @@ -28939,40 +28971,6 @@ } } }, - "skiing" : { - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "skitour" - } - }, - "it" : { - "stringUnit" : { - "state" : "translated", - "value" : "tour sciistico" - } - }, - "sr" : { - "stringUnit" : { - "state" : "translated", - "value" : "ски тура" - } - }, - "zh-Hans" : { - "stringUnit" : { - "state" : "translated", - "value" : "滑雪之旅" - } - }, - "zh-Hant-TW" : { - "stringUnit" : { - "state" : "translated", - "value" : "滑雪" - } - } - } - }, "Skiing" : { "localizations" : { "de" : { @@ -32959,6 +32957,28 @@ } } }, + "unknown" : { + "localizations" : { + "it" : { + "stringUnit" : { + "state" : "needs_review", + "value" : "sconosciuto" + } + }, + "sr" : { + "stringUnit" : { + "state" : "needs_review", + "value" : "непознато" + } + }, + "zh-Hant-TW" : { + "stringUnit" : { + "state" : "needs_review", + "value" : "未知" + } + } + } + }, "Unknown" : { "localizations" : { "fr" : { @@ -34343,40 +34363,6 @@ } } }, - "walk" : { - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "gehen" - } - }, - "it" : { - "stringUnit" : { - "state" : "translated", - "value" : "passeggiata" - } - }, - "sr" : { - "stringUnit" : { - "state" : "translated", - "value" : "шетња" - } - }, - "zh-Hans" : { - "stringUnit" : { - "state" : "translated", - "value" : "步行" - } - }, - "zh-Hant-TW" : { - "stringUnit" : { - "state" : "translated", - "value" : "走路" - } - } - } - }, "Walking" : { "localizations" : { "de" : { @@ -35509,485 +35495,7 @@ } } } - }, - "ble.signal.strength.weak" : { - "comment" : "VoiceOver value for weak BLE signal strength", - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "needs_review", - "value" : "Signalstärke schwach" - } - }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Signal strength weak" - } - }, - "it" : { - "stringUnit" : { - "state" : "needs_review", - "value" : "Segnale debole" - } - }, - "sr" : { - "stringUnit" : { - "state" : "needs_review", - "value" : "Слаб сигнал" - } - }, - "zh-Hans" : { - "stringUnit" : { - "state" : "needs_review", - "value" : "信号弱" - } - }, - "zh-Hant-TW" : { - "stringUnit" : { - "state" : "needs_review", - "value" : "訊號微弱" - } - } - } - }, - "signal_strength" : { - "comment" : "VoiceOver label for signal strength indicator", - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "needs_review", - "value" : "Signalstärke" - } - }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Signal strength" - } - }, - "it" : { - "stringUnit" : { - "state" : "needs_review", - "value" : "Intensità del segnale" - } - }, - "sr" : { - "stringUnit" : { - "state" : "needs_review", - "value" : "Јачина сигнала" - } - }, - "zh-Hans" : { - "stringUnit" : { - "state" : "needs_review", - "value" : "信号强度" - } - }, - "zh-Hant-TW" : { - "stringUnit" : { - "state" : "needs_review", - "value" : "訊號強度" - } - } - } - }, - "message_size" : { - "comment" : "VoiceOver label for message size", - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "needs_review", - "value" : "Nachrichtengröße" - } - }, - "en" : { - "stringUnit" : { - "state" : "needs_review", - "value" : "Message size" - } - }, - "it" : { - "stringUnit" : { - "state" : "needs_review", - "value" : "Dimensione messaggio" - } - }, - "sr" : { - "stringUnit" : { - "state" : "needs_review", - "value" : "Величина поруке" - } - }, - "zh-Hans" : { - "stringUnit" : { - "state" : "needs_review", - "value" : "消息大小" - } - }, - "zh-Hant-TW" : { - "stringUnit" : { - "state" : "needs_review", - "value" : "訊息大小" - } - } - } - }, - "device_charging" : { - "comment" : "VoiceOver value for charging device", - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Charging" - } - } - } - }, - "Bluetooth is off.off" : { - "extractionState" : "stale", - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Bluetooth ist aus" - } - }, - "fr" : { - "stringUnit" : { - "state" : "translated", - "value" : "Le Bluetooth est arrêté" - } - }, - "he" : { - "stringUnit" : { - "state" : "translated", - "value" : "בלוטוס כבוי" - } - }, - "it" : { - "stringUnit" : { - "state" : "translated", - "value" : "Il Bluetooth è spento" - } - }, - "pl" : { - "stringUnit" : { - "state" : "translated", - "value" : "Bluetooth jest wyłączony" - } - }, - "se" : { - "stringUnit" : { - "state" : "translated", - "value" : "Bluetooth är avstängt" - } - }, - "sr" : { - "stringUnit" : { - "state" : "translated", - "value" : "Блутут је искључен" - } - }, - "zh-Hans" : { - "stringUnit" : { - "state" : "translated", - "value" : "蓝牙已关闭" - } - }, - "zh-Hant-TW" : { - "stringUnit" : { - "state" : "translated", - "value" : "藍芽已關閉" - } - } - } - }, - "bytes_used" : { - "comment" : "VoiceOver value for bytes used", - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "needs_review", - "value" : "%d von %d Bytes verwendet" - } - }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "%d of %d bytes used" - } - }, - "it" : { - "stringUnit" : { - "state" : "needs_review", - "value" : "%d di %d byte usati" - } - }, - "sr" : { - "stringUnit" : { - "state" : "needs_review", - "value" : "%d од %d бајтова искоришћено" - } - }, - "zh-Hans" : { - "stringUnit" : { - "state" : "needs_review", - "value" : "已用%d/%d字节" - } - }, - "zh-Hant-TW" : { - "stringUnit" : { - "state" : "needs_review", - "value" : "已用%d/%d位元組" - } - } - } - }, - "heading" : { - "comment" : "Heading label for VoiceOver" - }, - "Hide sidebar" : {}, - "bluetooth.not.connected" : { - "comment" : "VoiceOver label for disconnected Bluetooth icon", - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "No Bluetooth device connected" - } - } - } - }, - "device.configuration" : { - "extractionState" : "stale", - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "needs_review", - "value" : "Gerätekonfiguration" - } - }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Device Configuration" - } - }, - "he" : { - "stringUnit" : { - "state" : "needs_review", - "value" : "Device Configuration" - } - }, - "it" : { - "stringUnit" : { - "state" : "needs_review", - "value" : "Configurazione del dispositivo" - } - }, - "pl" : { - "stringUnit" : { - "state" : "needs_review", - "value" : "Device Configuration" - } - }, - "se" : { - "stringUnit" : { - "state" : "needs_review", - "value" : "Enhetsinställningar" - } - }, - "sr" : { - "stringUnit" : { - "state" : "needs_review", - "value" : "Подешавања уређаја" - } - }, - "zh-Hans" : { - "stringUnit" : { - "state" : "needs_review", - "value" : "设备配置" - } - }, - "zh-Hant-TW" : { - "stringUnit" : { - "state" : "needs_review", - "value" : "設備設定" - } - } - } - }, - "ble.signal.strength.strong" : { - "comment" : "VoiceOver value for strong BLE signal strength", - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "needs_review", - "value" : "Signalstärke stark" - } - }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Signal strength strong" - } - }, - "it" : { - "stringUnit" : { - "state" : "needs_review", - "value" : "Segnale forte" - } - }, - "sr" : { - "stringUnit" : { - "state" : "needs_review", - "value" : "Јак сигнал" - } - }, - "zh-Hans" : { - "stringUnit" : { - "state" : "needs_review", - "value" : "信号强" - } - }, - "zh-Hant-TW" : { - "stringUnit" : { - "state" : "needs_review", - "value" : "訊號強" - } - } - } - }, - "ble.signal.strength.normal" : { - "comment" : "VoiceOver value for normal BLE signal strength", - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "needs_review", - "value" : "Signalstärke normal" - } - }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Signal strength normal" - } - }, - "it" : { - "stringUnit" : { - "state" : "needs_review", - "value" : "Segnale normale" - } - }, - "sr" : { - "stringUnit" : { - "state" : "needs_review", - "value" : "Нормалан сигнал" - } - }, - "zh-Hans" : { - "stringUnit" : { - "state" : "needs_review", - "value" : "信号正常" - } - }, - "zh-Hant-TW" : { - "stringUnit" : { - "state" : "needs_review", - "value" : "訊號正常" - } - } - } - }, - "bluetooth.connected" : { - "comment" : "VoiceOver label for connected Bluetooth icon", - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Connected to Bluetooth device" - } - } - } - }, - "request_position" : { - "comment" : "VoiceOver label for request position button", - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Position anfordern" - } - }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Request position" - } - }, - "it" : { - "stringUnit" : { - "state" : "translated", - "value" : "Richiedi posizione" - } - }, - "sr" : { - "stringUnit" : { - "state" : "translated", - "value" : "Захтевај позицију" - } - }, - "zh-Hans" : { - "stringUnit" : { - "state" : "translated", - "value" : "请求位置" - } - }, - "zh-Hant-TW" : { - "stringUnit" : { - "state" : "translated", - "value" : "請求位置" - } - } - } - }, - "distance" : { - "comment" : "Distance label for VoiceOver" - }, - "device_plugged_in" : { - "comment" : "VoiceOver value for plugged in device", - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Plugged in" - } - } - } - }, - "unknown" : { - "localizations" : { - "it" : { - "stringUnit" : { - "state" : "needs_review", - "value" : "sconosciuto" - } - }, - "sr" : { - "stringUnit" : { - "state" : "needs_review", - "value" : "непознато" - } - }, - "zh-Hant-TW" : { - "stringUnit" : { - "state" : "needs_review", - "value" : "未知" - } - } - } } }, "version" : "1.0" -} +} \ No newline at end of file diff --git a/Meshtastic/Enums/RouteEnums.swift b/Meshtastic/Enums/RouteEnums.swift index 06876b4a..95b9c642 100644 --- a/Meshtastic/Enums/RouteEnums.swift +++ b/Meshtastic/Enums/RouteEnums.swift @@ -37,17 +37,17 @@ enum ActivityType: Int, CaseIterable, Identifiable { var fileNameString: String { switch self { case .walking: - return "walk".localized + return "Walking".localized.lowercased() case .hiking: - return "hiking".localized + return "Hiking".localized.lowercased() case .biking: - return "biking".localized + return "Biking".localized.lowercased() case .driving: - return "driving".localized + return "Driving".localized.lowercased() case .overlanding: - return "overlanding".localized + return "Overlanding".localized.lowercased() case .skiing: - return "skiing".localized + return "Skiing".localized.lowercased() } } } diff --git a/Meshtastic/Extensions/String.swift b/Meshtastic/Extensions/String.swift index 6a57da9e..9ade6991 100644 --- a/Meshtastic/Extensions/String.swift +++ b/Meshtastic/Extensions/String.swift @@ -123,7 +123,7 @@ extension String { with: "$1 $2", options: .regularExpression ) - return "Node " + spaced + return "Node".localized + " " + spaced } // Adds variation selectors to prefer the graphical form of emoji. diff --git a/Meshtastic/Views/Helpers/BLESignalStrengthIndicator.swift b/Meshtastic/Views/Helpers/BLESignalStrengthIndicator.swift index 6b0287b0..e8efaf3e 100644 --- a/Meshtastic/Views/Helpers/BLESignalStrengthIndicator.swift +++ b/Meshtastic/Views/Helpers/BLESignalStrengthIndicator.swift @@ -58,7 +58,7 @@ struct SignalStrengthIndicator: View { } } .accessibilityElement(children: .ignore) - .accessibilityLabel(NSLocalizedString("signal_strength", comment: "VoiceOver label for signal strength indicator")) + .accessibilityLabel("Signal strength".localized) .accessibilityValue(accessibilityDescription) } diff --git a/Meshtastic/Views/Helpers/BatteryCompact.swift b/Meshtastic/Views/Helpers/BatteryCompact.swift index bb9819a2..6d31e6af 100644 --- a/Meshtastic/Views/Helpers/BatteryCompact.swift +++ b/Meshtastic/Views/Helpers/BatteryCompact.swift @@ -101,7 +101,7 @@ struct BatteryCompact: View { .accessibilityLabel(NSLocalizedString("Battery Level", comment: "VoiceOver label for battery gauge")) // Set appropriate value based on the battery state using a computed property .accessibilityValue(batteryLevel.map { level in - if level > 100 { + if level > 100 { // Plugged in - same as PWD visual indicator return NSLocalizedString("device_plugged_in", comment: "VoiceOver value for plugged in device") } else if level == 100 { @@ -109,7 +109,7 @@ struct BatteryCompact: View { return NSLocalizedString("device_charging", comment: "VoiceOver value for charging device") } else { // Normal battery level - return String(format: NSLocalizedString("battery_level_percent", comment: "VoiceOver value for battery level"), Int(level)) + return String(format: NSLocalizedString("Battery Level %", comment: "VoiceOver value for battery level"), Int(level)) } } ?? "Unknown") } diff --git a/Meshtastic/Views/Messages/TextMessageField/RequestPositionButton.swift b/Meshtastic/Views/Messages/TextMessageField/RequestPositionButton.swift index fd166f51..fac95a11 100644 --- a/Meshtastic/Views/Messages/TextMessageField/RequestPositionButton.swift +++ b/Meshtastic/Views/Messages/TextMessageField/RequestPositionButton.swift @@ -6,7 +6,7 @@ struct RequestPositionButton: View { var body: some View { Button(action: action) { Image(systemName: "mappin.and.ellipse") - .accessibilityLabel(NSLocalizedString("request_position", comment: "VoiceOver label for request position button")) + .accessibilityLabel("Position Exchange Requested".localized) .symbolRenderingMode(.hierarchical) .imageScale(.large) .foregroundColor(.accentColor) diff --git a/Meshtastic/Views/Messages/TextMessageField/TextMessageSize.swift b/Meshtastic/Views/Messages/TextMessageField/TextMessageSize.swift index 9839e246..c6a0032f 100644 --- a/Meshtastic/Views/Messages/TextMessageField/TextMessageSize.swift +++ b/Meshtastic/Views/Messages/TextMessageField/TextMessageSize.swift @@ -6,8 +6,8 @@ struct TextMessageSize: View { var body: some View { ProgressView("\("Bytes".localized): \(totalBytes) / \(maxbytes)", value: Double(totalBytes), total: Double(maxbytes)) - .accessibilityLabel(NSLocalizedString("message_size", comment: "VoiceOver label for message size")) - .accessibilityValue(String(format: NSLocalizedString("bytes_used", comment: "VoiceOver value for bytes used"), totalBytes, maxbytes)) + .accessibilityLabel(NSLocalizedString("Message Size", comment: "VoiceOver label for message size")) + .accessibilityValue(String(format: NSLocalizedString("Bytes Used", comment: "VoiceOver value for bytes used"), totalBytes, maxbytes)) .frame(width: 130) .padding(5) .font(.subheadline) diff --git a/Meshtastic/Views/Nodes/Helpers/NodeListItem.swift b/Meshtastic/Views/Nodes/Helpers/NodeListItem.swift index 62dd5fd0..a25d9d51 100644 --- a/Meshtastic/Views/Nodes/Helpers/NodeListItem.swift +++ b/Meshtastic/Views/Nodes/Helpers/NodeListItem.swift @@ -20,7 +20,7 @@ struct NodeListItem: View { } else if let longName = node.user?.longName { desc = longName } else { - desc = "unknown node" + desc = "Unknown".localized + " " + "Node".localized } if connected { desc += ", currently connected" @@ -66,14 +66,13 @@ struct NodeListItem: View { distanceFormatter.unitStyle = .medium let formattedDistance = distanceFormatter.string(fromMeters: metersAway) // For VoiceOver, prepend 'Distance' (localized) - desc += ", " + String(format: "%@: %@", NSLocalizedString("distance", comment: "Distance label for VoiceOver"), formattedDistance) - + desc += ", " + String(format: "%@: %@", "Distance".localized, formattedDistance) // Add bearing/heading information for VoiceOver let trueBearing = getBearingBetweenTwoPoints(point1: myCoord, point2: nodeCoord) let heading = Measurement(value: trueBearing, unit: UnitAngle.degrees) let formattedHeading = heading.formatted(.measurement(width: .narrow, numberFormatStyle: .number.precision(.fractionLength(0)))) // Using a direct format without requiring a new localization key - desc += ", " + NSLocalizedString("heading", comment: "Heading label for VoiceOver") + " " + formattedHeading + desc += ", " + "Heading".localized + " " + formattedHeading } // Add signal strength if available if node.snr != 0 && !node.viaMqtt { @@ -99,7 +98,6 @@ struct NodeListItem: View { return desc } - @ObservedObject var node: NodeInfoEntity var connected: Bool var connectedNode: Int64 From d7d2378c51630eb16fbc23fecc6a8580137db943 Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Tue, 20 May 2025 22:29:53 -0700 Subject: [PATCH 051/213] Ben thought he was writing C# --- Localizable.xcstrings | 167 ------------------ Meshtastic/AppIntents/AddContactIntent.swift | 3 - .../Helpers/BLESignalStrengthIndicator.swift | 6 +- Meshtastic/Views/Helpers/BatteryCompact.swift | 4 +- Meshtastic/Views/Helpers/BatteryGauge.swift | 4 +- .../Views/Helpers/ConnectedDevice.swift | 4 +- .../Views/Messages/UserMessageList.swift | 8 +- .../Views/Nodes/Helpers/NodeListItem.swift | 11 +- .../Nodes/Helpers/ShareContactQRDialog.swift | 9 +- 9 files changed, 19 insertions(+), 197 deletions(-) diff --git a/Localizable.xcstrings b/Localizable.xcstrings index f08588ee..ba8240c5 100644 --- a/Localizable.xcstrings +++ b/Localizable.xcstrings @@ -4419,129 +4419,6 @@ } } }, - "ble.signal.strength.normal" : { - "comment" : "VoiceOver value for normal BLE signal strength", - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "needs_review", - "value" : "Signalstärke normal" - } - }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Signal strength normal" - } - }, - "it" : { - "stringUnit" : { - "state" : "needs_review", - "value" : "Segnale normale" - } - }, - "sr" : { - "stringUnit" : { - "state" : "needs_review", - "value" : "Нормалан сигнал" - } - }, - "zh-Hans" : { - "stringUnit" : { - "state" : "needs_review", - "value" : "信号正常" - } - }, - "zh-Hant-TW" : { - "stringUnit" : { - "state" : "needs_review", - "value" : "訊號正常" - } - } - } - }, - "ble.signal.strength.strong" : { - "comment" : "VoiceOver value for strong BLE signal strength", - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "needs_review", - "value" : "Signalstärke stark" - } - }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Signal strength strong" - } - }, - "it" : { - "stringUnit" : { - "state" : "needs_review", - "value" : "Segnale forte" - } - }, - "sr" : { - "stringUnit" : { - "state" : "needs_review", - "value" : "Јак сигнал" - } - }, - "zh-Hans" : { - "stringUnit" : { - "state" : "needs_review", - "value" : "信号强" - } - }, - "zh-Hant-TW" : { - "stringUnit" : { - "state" : "needs_review", - "value" : "訊號強" - } - } - } - }, - "ble.signal.strength.weak" : { - "comment" : "VoiceOver value for weak BLE signal strength", - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "needs_review", - "value" : "Signalstärke schwach" - } - }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Signal strength weak" - } - }, - "it" : { - "stringUnit" : { - "state" : "needs_review", - "value" : "Segnale debole" - } - }, - "sr" : { - "stringUnit" : { - "state" : "needs_review", - "value" : "Слаб сигнал" - } - }, - "zh-Hans" : { - "stringUnit" : { - "state" : "needs_review", - "value" : "信号弱" - } - }, - "zh-Hant-TW" : { - "stringUnit" : { - "state" : "needs_review", - "value" : "訊號微弱" - } - } - } - }, "Bluetooth" : { "localizations" : { "de" : { @@ -4774,28 +4651,6 @@ } } }, - "bluetooth.connected" : { - "comment" : "VoiceOver label for connected Bluetooth icon", - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Connected to Bluetooth device" - } - } - } - }, - "bluetooth.not.connected" : { - "comment" : "VoiceOver label for disconnected Bluetooth icon", - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "No Bluetooth device connected" - } - } - } - }, "Broadcast Interval" : { "localizations" : { "it" : { @@ -9462,28 +9317,6 @@ } } }, - "device_charging" : { - "comment" : "VoiceOver value for charging device", - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Charging" - } - } - } - }, - "device_plugged_in" : { - "comment" : "VoiceOver value for plugged in device", - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Plugged in" - } - } - } - }, "Dilution of precision (DOP) PDOP used by default" : { "localizations" : { "it" : { diff --git a/Meshtastic/AppIntents/AddContactIntent.swift b/Meshtastic/AppIntents/AddContactIntent.swift index 17c6b960..e68ac4a3 100644 --- a/Meshtastic/AppIntents/AddContactIntent.swift +++ b/Meshtastic/AppIntents/AddContactIntent.swift @@ -23,11 +23,9 @@ struct AddContactIntent: AppIntent { } if contactUrl.absoluteString.lowercased().contains("meshtastic.org/v/#") { - let components = self.contactUrl.absoluteString.components(separatedBy: "#") // Extract contact information from the URL if let contactData = components.last { - let decodedString = contactData.base64urlToBase64() if let decodedData = Data(base64Encoded: decodedString) { do { @@ -38,7 +36,6 @@ struct AddContactIntent: AppIntent { } catch { throw AppIntentErrors.AppIntentError.message("Failed to parse contact data: \(error.localizedDescription)") - } } } diff --git a/Meshtastic/Views/Helpers/BLESignalStrengthIndicator.swift b/Meshtastic/Views/Helpers/BLESignalStrengthIndicator.swift index e8efaf3e..2e8d5c53 100644 --- a/Meshtastic/Views/Helpers/BLESignalStrengthIndicator.swift +++ b/Meshtastic/Views/Helpers/BLESignalStrengthIndicator.swift @@ -36,11 +36,11 @@ struct SignalStrengthIndicator: View { private var accessibilityDescription: String { switch signalStrength { case .weak: - return NSLocalizedString("ble.signal.strength.weak", comment: "VoiceOver value for weak BLE signal strength") + return "Signal strength weak".localized case .normal: - return NSLocalizedString("ble.signal.strength.normal", comment: "VoiceOver value for normal BLE signal strength") + return "Signal strength normal".localized case .strong: - return NSLocalizedString("ble.signal.strength.strong", comment: "VoiceOver value for strong BLE signal strength") + return "Signal strength strong".localized } } diff --git a/Meshtastic/Views/Helpers/BatteryCompact.swift b/Meshtastic/Views/Helpers/BatteryCompact.swift index 6d31e6af..f2142534 100644 --- a/Meshtastic/Views/Helpers/BatteryCompact.swift +++ b/Meshtastic/Views/Helpers/BatteryCompact.swift @@ -103,10 +103,10 @@ struct BatteryCompact: View { .accessibilityValue(batteryLevel.map { level in if level > 100 { // Plugged in - same as PWD visual indicator - return NSLocalizedString("device_plugged_in", comment: "VoiceOver value for plugged in device") + return "Plugged in".localized } else if level == 100 { // Charging - same as CHG visual indicator - return NSLocalizedString("device_charging", comment: "VoiceOver value for charging device") + return "Charging".localized } else { // Normal battery level return String(format: NSLocalizedString("Battery Level %", comment: "VoiceOver value for battery level"), Int(level)) diff --git a/Meshtastic/Views/Helpers/BatteryGauge.swift b/Meshtastic/Views/Helpers/BatteryGauge.swift index 81e81e7e..addbc97c 100644 --- a/Meshtastic/Views/Helpers/BatteryGauge.swift +++ b/Meshtastic/Views/Helpers/BatteryGauge.swift @@ -79,8 +79,8 @@ struct PluggedInIndicator: View { .symbolRenderingMode(.hierarchical) // Override the accessibility to ensure correct VoiceOver announcement .accessibilityElement(children: .ignore) - .accessibilityLabel(NSLocalizedString("Battery Level", comment: "VoiceOver label for battery gauge")) - .accessibilityValue(NSLocalizedString("device_plugged_in", comment: "VoiceOver value for plugged in device")) + .accessibilityLabel("Battery Level".localized) + .accessibilityValue("Plugged in".localized) } } diff --git a/Meshtastic/Views/Helpers/ConnectedDevice.swift b/Meshtastic/Views/Helpers/ConnectedDevice.swift index 4a46db41..e0dc8a02 100644 --- a/Meshtastic/Views/Helpers/ConnectedDevice.swift +++ b/Meshtastic/Views/Helpers/ConnectedDevice.swift @@ -38,7 +38,7 @@ struct ConnectedDevice: View { .accessibilityHidden(true) } .accessibilityElement(children: .ignore) - .accessibilityLabel("bluetooth.connected".localized + ", " + name.formatNodeNameForVoiceOver()) + .accessibilityLabel("Connected to Bluetooth device".localized + ", " + name.formatNodeNameForVoiceOver()) } else { // Create a container for disconnected state HStack { @@ -49,7 +49,7 @@ struct ConnectedDevice: View { .accessibilityHidden(true) } .accessibilityElement(children: .ignore) - .accessibilityLabel("bluetooth.not.connected".localized) + .accessibilityLabel("No Bluetooth device connected".localized) } } else { // Create a container for Bluetooth off state diff --git a/Meshtastic/Views/Messages/UserMessageList.swift b/Meshtastic/Views/Messages/UserMessageList.swift index 54ff0b4d..65854fc0 100644 --- a/Meshtastic/Views/Messages/UserMessageList.swift +++ b/Meshtastic/Views/Messages/UserMessageList.swift @@ -14,7 +14,6 @@ struct UserMessageList: View { @EnvironmentObject var appState: AppState @EnvironmentObject var bleManager: BLEManager @Environment(\.managedObjectContext) var context - // Keyboard State @FocusState var messageFieldFocused: Bool // View State Items @@ -24,23 +23,22 @@ struct UserMessageList: View { @State private var showScrollToBottomButton = false @State private var hasReachedBottom = false @State private var gotFirstUnreadMessage: Bool = false - @State private var messageToHighlight: Int64 = 0 - + var body: some View { VStack { ScrollViewReader { scrollView in ZStack(alignment: .bottomTrailing) { ScrollView { LazyVStack { - ForEach( Array(user.messageList.enumerated()) , id: \.element.id) { index, message in + ForEach( Array(user.messageList.enumerated()), id: \.element.id) { index, message in // Get the previous message, if it exists let previousMessage = index > 0 ? user.messageList[index - 1] : nil if message.displayTimestamp(aboveMessage: previousMessage) { Text(message.timestamp.formatted(date: .abbreviated, time: .shortened)) .font(.caption) .foregroundColor(.gray) - } + } if user.num != bleManager.connectedPeripheral?.num ?? -1 { let currentUser: Bool = (Int64(UserDefaults.preferredPeripheralNum) == message.fromUser?.num ?? -1 ? true : false) diff --git a/Meshtastic/Views/Nodes/Helpers/NodeListItem.swift b/Meshtastic/Views/Nodes/Helpers/NodeListItem.swift index a25d9d51..e7d00a6a 100644 --- a/Meshtastic/Views/Nodes/Helpers/NodeListItem.swift +++ b/Meshtastic/Views/Nodes/Helpers/NodeListItem.swift @@ -49,9 +49,9 @@ struct NodeListItem: View { if let battery = node.latestDeviceMetrics?.batteryLevel { // Check for plugged in and charging states, same logic as in BatteryCompact and BatteryGauge if battery > 100 { - desc += ", " + NSLocalizedString("device_plugged_in", comment: "VoiceOver value for plugged in device") + desc += ", " + "Plugged in".localized } else if battery == 100 { - desc += ", " + NSLocalizedString("device_charging", comment: "VoiceOver value for charging device") + desc += ", " + "Charging".localized } else { desc += ", battery \(battery)%" } @@ -60,7 +60,6 @@ struct NodeListItem: View { if !connected, let (lastPosition, myCoord) = locationData { let nodeCoord = CLLocation(latitude: lastPosition.nodeCoordinate!.latitude, longitude: lastPosition.nodeCoordinate!.longitude) let metersAway = nodeCoord.distance(from: myCoord) - // Distance information let distanceFormatter = LengthFormatter() distanceFormatter.unitStyle = .medium @@ -87,11 +86,11 @@ struct NodeListItem: View { let signalString: String switch signalStrength { case .weak: - signalString = NSLocalizedString("ble.signal.strength.weak", comment: "VoiceOver value for weak BLE signal strength") + signalString = "Signal strength weak".localized case .normal: - signalString = NSLocalizedString("ble.signal.strength.normal", comment: "VoiceOver value for normal BLE signal strength") + signalString = "Signal strength normal".localized case .strong: - signalString = NSLocalizedString("ble.signal.strength.strong", comment: "VoiceOver value for strong BLE signal strength") + signalString = "Signal strength strong".localized } desc += ", " + signalString } diff --git a/Meshtastic/Views/Nodes/Helpers/ShareContactQRDialog.swift b/Meshtastic/Views/Nodes/Helpers/ShareContactQRDialog.swift index 5d361fbe..4bc56e24 100644 --- a/Meshtastic/Views/Nodes/Helpers/ShareContactQRDialog.swift +++ b/Meshtastic/Views/Nodes/Helpers/ShareContactQRDialog.swift @@ -15,12 +15,10 @@ import OSLog struct ShareContactQRDialog: View { let node: NodeInfo @Environment(\.dismiss) private var dismiss - var qrString: String { var contact = SharedContact() contact.nodeNum = node.num contact.user = node.user - do { let contactString = try contact.serializedData().base64EncodedString() return ("https://meshtastic.org/v/#" + contactString.base64ToBase64url()) @@ -28,9 +26,7 @@ struct ShareContactQRDialog: View { Logger.services.error("Error serializing contact: \(error)") return "" } - } - var qrImage: UIImage { let context = CIContext() let filter = CIFilter.qrCodeGenerator() @@ -42,7 +38,6 @@ struct ShareContactQRDialog: View { } return UIImage(systemName: "xmark.circle") ?? UIImage() } - var body: some View { VStack(spacing: 20) { Text("Share Contact QR") @@ -86,8 +81,8 @@ struct ShareContactQRDialog_Previews: PreviewProvider { userProto.id = "!1234" userProto.longName = "Bud" userProto.shortName = "Bud" - userProto.hwModel = HardwareModel(rawValue:1)!; - userProto.role = Config.DeviceConfig.Role(rawValue: 1)! + userProto.hwModel = HardwareModel.tbeam + userProto.role = Config.DeviceConfig.Role.client userProto.publicKey = Data() node.user = userProto From 2f76e88ea532a0260ec33ce78d003d1b5986df07 Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Tue, 20 May 2025 22:33:17 -0700 Subject: [PATCH 052/213] Revert "Update serbian translations" --- Localizable.xcstrings | 260 +----------------- Meshtastic/Extensions/UserDefaults.swift | 4 +- Meshtastic/Tips/BluetoothTips.swift | 2 +- .../Views/Messages/ChannelMessageList.swift | 1 - .../Views/Messages/UserMessageList.swift | 4 +- .../Config/Module/StoreForwardConfig.swift | 2 +- 6 files changed, 12 insertions(+), 261 deletions(-) diff --git a/Localizable.xcstrings b/Localizable.xcstrings index a5c4316f..f08588ee 100644 --- a/Localizable.xcstrings +++ b/Localizable.xcstrings @@ -2305,11 +2305,6 @@ }, "Administration Enabled" : { "localizations" : { - "sr" : { - "stringUnit" : { - "state" : "translated", - "value" : "Администрација је активирана" - }, "zh-Hant-TW" : { "stringUnit" : { "state" : "translated", @@ -5089,12 +5084,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" : { - "sr" : { - "stringUnit" : { - "state" : "translated", - "value" : "Укључивањем ове функције, потврђујете и изричито пристајете на пренос географске локације вашег уређаја у реалном времену преко MQTT протокола без шифровања. Ови подаци о локацији могу се користити у сврхе као што су извештавање на живој мапи, праћење уређаја и сродне телеметријске функције." - } - }, "zh-Hant-TW" : { "stringUnit" : { "state" : "translated", @@ -6262,12 +6251,6 @@ "value" : "Utilizzo del canale %@%%" } }, - "sr" : { - "stringUnit" : { - "state" : "translated", - "value" : "Искоришћеност канала %@%%" - } - }, "zh-Hant-TW" : { "stringUnit" : { "state" : "translated", @@ -6894,12 +6877,6 @@ "value" : "Supporto alla community" } }, - "sr" : { - "stringUnit" : { - "state" : "translated", - "value" : "Подршка заједнице" - } - }, "zh-Hant-TW" : { "stringUnit" : { "state" : "translated", @@ -7028,12 +7005,6 @@ "value" : "Conferma" } }, - "sr" : { - "stringUnit" : { - "state" : "translated", - "value" : "Потврди" - } - }, "zh-Hant-TW" : { "stringUnit" : { "state" : "translated", @@ -7072,12 +7043,6 @@ }, "Connect to MQTT via Proxy" : { "localizations" : { - "sr" : { - "stringUnit" : { - "state" : "translated", - "value" : "Повежите се на MQTT преко проксија" - } - }, "zh-Hant-TW" : { "stringUnit" : { "state" : "translated", @@ -7094,12 +7059,6 @@ "value" : "Collegare alla nuova radio?" } }, - "sr" : { - "stringUnit" : { - "state" : "translated", - "value" : "Повезати се на нови радио уређај?" - } - }, "zh-Hant-TW" : { "stringUnit" : { "state" : "translated", @@ -7202,12 +7161,6 @@ "value" : "Radio connessa" } }, - "sr" : { - "stringUnit" : { - "state" : "translated", - "value" : "Радио повезан" - } - }, "zh-Hant-TW" : { "stringUnit" : { "state" : "translated", @@ -7282,12 +7235,6 @@ "value" : "La connessione a una nuova radio cancellerà tutti i dati delle app sul telefono." } }, - "sr" : { - "stringUnit" : { - "state" : "translated", - "value" : "Повезивање са новим радиом ће обрисати све податке апликације на телефону." - } - }, "zh-Hant-TW" : { "stringUnit" : { "state" : "translated", @@ -7390,12 +7337,6 @@ }, "Consent to Share Unencrypted Node Data via MQTT" : { "localizations" : { - "sr" : { - "stringUnit" : { - "state" : "translated", - "value" : "Пристанак на дељење нешифрованих података чвора путем MQTT-а" - } - }, "zh-Hant-TW" : { "stringUnit" : { "state" : "translated", @@ -7942,12 +7883,6 @@ "value" : "Attualmente mostra i moduli che potrebbero non essere supportati da questo nodo." } }, - "sr" : { - "stringUnit" : { - "state" : "translated", - "value" : "Тренутно приказује модуле који можда нису подржани од стране овог чвора." - } - }, "zh-Hant-TW" : { "stringUnit" : { "state" : "translated", @@ -10717,12 +10652,6 @@ "value" : "Abilita la trasmissione di pacchetti via UDP sulla rete locale." } }, - "sr" : { - "stringUnit" : { - "state" : "translated", - "value" : "Омогућите емитовање пакета путем UDP-а преко локалне мреже." - } - }, "zh-Hant-TW" : { "stringUnit" : { "state" : "translated", @@ -10767,12 +10696,6 @@ "value" : "Abilita questo dispositivo come server Store and Forward. Richiede un dispositivo ESP32 con PSRAM." } }, - "sr" : { - "stringUnit" : { - "state" : "translated", - "value" : "Омогућите овај уређај као сервер за складиштење и прослеђивање. Захтева ESP32 уређај са PSRAM-ом." - } - }, "zh-Hant-TW" : { "stringUnit" : { "state" : "translated", @@ -10955,12 +10878,6 @@ }, "Enables the store and forward module." : { "localizations" : { - "sr" : { - "stringUnit" : { - "state" : "translated", - "value" : "Омогућава модул за складиштење и прослеђивање." - } - }, "zh-Hant-TW" : { "stringUnit" : { "state" : "translated", @@ -13385,12 +13302,6 @@ "value" : "Supporto completo" } }, - "sr" : { - "stringUnit" : { - "state" : "translated", - "value" : "Пуна подршка" - } - }, "zh-Hant-TW" : { "stringUnit" : { "state" : "translated", @@ -14934,12 +14845,6 @@ }, "I have read and understand the above. I voluntarily consent to the unencrypted transmission of my node data via MQTT." : { "localizations" : { - "sr" : { - "stringUnit" : { - "state" : "translated", - "value" : "Прочитао/ла сам и разумем горе наведено. Добровољно пристајем на нешифровани пренос података мог чвора путем MQTT-а." - } - }, "zh-Hant-TW" : { "stringUnit" : { "state" : "translated", @@ -15286,12 +15191,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" : { - "sr" : { - "stringUnit" : { - "state" : "translated", - "value" : "Игнорише посматране поруке из страних мрежа попут Local Only, али иде и корак даље тако што такође игнорише поруке са чворова који се већ не налазе на листи познатих чворова." - } - }, "zh-Hant-TW" : { "stringUnit" : { "state" : "translated", @@ -15302,12 +15201,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" : { - "sr" : { - "stringUnit" : { - "state" : "translated", - "value" : "Игнорише посматране поруке из страних мрежа које су отворене или које не може дешифровати. Пребацује поруке само на примарним/секундарним каналима локалног чвора." - } - }, "zh-Hant-TW" : { "stringUnit" : { "state" : "translated", @@ -15840,12 +15733,6 @@ }, "Jump to present" : { "localizations" : { - "sr" : { - "stringUnit" : { - "state" : "translated", - "value" : "Скочи на најновије" - } - }, "zh-Hant-TW" : { "stringUnit" : { "state" : "translated", @@ -20949,12 +20836,6 @@ }, "Only permitted for SENSOR, TRACKER and TAK_TRACKER roles, this will inhibit all rebroadcasts, not unlike CLIENT_MUTE role." : { "localizations" : { - "sr" : { - "stringUnit" : { - "state" : "translated", - "value" : "Дозвољено само за улоге SENSOR, TRACKER и TAK_TRACKER, ово ће спречити сва поновна емитовања, слично улози CLIENT_MUTE." - } - }, "zh-Hant-TW" : { "stringUnit" : { "state" : "translated", @@ -20965,12 +20846,6 @@ }, "Only rebroadcasts packets from the core portnums: NodeInfo, Text, Position, Telemetry, and Routing." : { "localizations" : { - "sr" : { - "stringUnit" : { - "state" : "translated", - "value" : "Само поново емитује пакете из основних портова: Информације о чвору, Текст, Позиција, Телеметрија и Рутирање." - } - }, "zh-Hant-TW" : { "stringUnit" : { "state" : "translated", @@ -21931,6 +21806,12 @@ }, "paxcounter.log" : { "localizations" : { + "zh-Hant-TW" : { + "stringUnit" : { + "state" : "translated", + "value" : "paxcounter.log" + } + } } }, "Perform a factory reset on the node you are connected to" : { @@ -22187,12 +22068,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" : { - "sr" : { - "stringUnit" : { - "state" : "translated", - "value" : "Молимо вас да имате у виду да, пошто извештај мапе није шифрован, ваши подаци могу бити трајно сачувани и приказани од стране трећих лица. Meshtastic не преузима одговорност за такво чување, приказивање или откривање ових података." - } - }, "zh-Hant-TW" : { "stringUnit" : { "state" : "translated", @@ -23163,12 +23038,6 @@ "value" : "Pressione" } }, - "sr" : { - "stringUnit" : { - "state" : "translated", - "value" : "Притисак" - } - }, "zh-Hant-TW" : { "stringUnit" : { "state" : "translated", @@ -23647,12 +23516,6 @@ "value" : "Radiazioni" } }, - "sr" : { - "stringUnit" : { - "state" : "translated", - "value" : "Зрачење" - } - }, "zh-Hant-TW" : { "stringUnit" : { "state" : "translated", @@ -24091,12 +23954,6 @@ }, "Rebroadcast any observed message, if it was on our private channel or from another mesh with the same lora params." : { "localizations" : { - "sr" : { - "stringUnit" : { - "state" : "translated", - "value" : "Поново емитује сваку посматрану поруку, ако је била на нашем приватном каналу или из друге мреже са истим лора параметрима." - } - }, "zh-Hant-TW" : { "stringUnit" : { "state" : "translated", @@ -24759,11 +24616,6 @@ }, "Replying to a message" : { "localizations" : { - "sr" : { - "stringUnit" : { - "state" : "translated", - "value" : "Одговара на поруку" - }, "zh-Hant-TW" : { "stringUnit" : { "state" : "translated", @@ -25904,12 +25756,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" : { - "sr" : { - "stringUnit" : { - "state" : "translated", - "value" : "Исто понашање као ALL, али прескаче декодирање пакета и једноставно их поново емитује. Доступно само у улози Repeater. Подешавање овога на било којој другој улози резултираће понашањем ALL." - } - }, "zh-Hant-TW" : { "stringUnit" : { "state" : "translated", @@ -26708,11 +26554,6 @@ }, "Select a node from the drop down to manage connected or remote devices." : { "localizations" : { - "sr" : { - "stringUnit" : { - "state" : "translated", - "value" : "Изаберите чвор из падајућег менија за управљање повезаним или удаљеним уређајима." - }, "zh-Hant-TW" : { "stringUnit" : { "state" : "translated", @@ -26959,12 +26800,6 @@ "value" : "Invia un heartbeat per pubblicizzare la presenza del server." } }, - "sr" : { - "stringUnit" : { - "state" : "translated", - "value" : "Пошаљите откуцај срца за оглашавање присуства сервера." - } - }, "zh-Hant-TW" : { "stringUnit" : { "state" : "translated", @@ -28092,12 +27927,6 @@ "value" : "Opzione server" } }, - "sr" : { - "stringUnit" : { - "state" : "translated", - "value" : "Опција сервера" - } - }, "zh-Hant-TW" : { "stringUnit" : { "state" : "translated", @@ -28838,12 +28667,6 @@ "value" : "Mostra le informazioni relative alla radio Lora collegata via bluetooth. È possibile scorrere il dito verso sinistra per scollegare la radio e premere a lungo per avviare l'attività live." } }, - "sr" : { - "stringUnit" : { - "state" : "translated", - "value" : "Приказује информације за Лора радио повезан путем блутута. Можете превући улево да прекинете везу са радиом, а дугим притиском почети активност уживо." - } - }, "zh-Hant-TW" : { "stringUnit" : { "state" : "translated", @@ -29278,12 +29101,6 @@ "value" : "Umidità del suolo" } }, - "sr" : { - "stringUnit" : { - "state" : "translated", - "value" : "Влажност земљишта" - } - }, "zh-Hant-TW" : { "stringUnit" : { "state" : "translated", @@ -29300,12 +29117,6 @@ "value" : "Temperatura del suolo" } }, - "sr" : { - "stringUnit" : { - "state" : "translated", - "value" : "Температура земљишта" - } - }, "zh-Hant-TW" : { "stringUnit" : { "state" : "translated", @@ -29782,12 +29593,6 @@ "value" : "I server Store and Forward richiedono un dispositivo ESP32 con PSRAM o Linux Native." } }, - "sr" : { - "stringUnit" : { - "state" : "translated", - "value" : "Сервери за складиштење и прослеђивање захтевају ESP32 уређај са PSRAM-ом или Linux Native." - } - }, "zh-Hant-TW" : { "stringUnit" : { "state" : "translated", @@ -31078,12 +30883,6 @@ "value" : "I ruoli di router sono progettati per posizioni elevate, come le cime delle montagne e le torri. Questo nodo deve essere in grado di avere una buona connessione diretta con la maggior parte dei nodi della rete, altrimenti danneggia significativamente la rete." } }, - "sr" : { - "stringUnit" : { - "state" : "translated", - "value" : "Улоге рутера су дизајниране за локације са добром прегледношћу попут планинских врхова и торњева. Овај чвор мора имати добру директну везу са већином чворова на мрежи, иначе ће значајно нарушити мрежу." - } - }, "zh-Hant-TW" : { "stringUnit" : { "state" : "translated", @@ -31584,12 +31383,6 @@ "value" : "Questo nodo non supporta alcun modulo configurabile." } }, - "sr" : { - "stringUnit" : { - "state" : "translated", - "value" : "Овај чвор не подржава ниједан конфигурабилни модул." - } - }, "zh-Hant-TW" : { "stringUnit" : { "state" : "translated", @@ -32160,12 +31953,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" : { - "sr" : { - "stringUnit" : { - "state" : "translated", - "value" : "У складу са законима о приватности попут CCPA и GDPR, избегавамо дељење тачних података о локацији. Уместо тога, користимо анонимизоване или приближне (непрецизне) информације о локацији како бисмо заштитили вашу приватност." - } - }, "zh-Hant-TW" : { "stringUnit" : { "state" : "translated", @@ -32950,12 +32737,6 @@ "value" : "Trasmissione UDP" } }, - "sr" : { - "stringUnit" : { - "state" : "translated", - "value" : "УДП емитовање" - } - }, "zh-Hant-TW" : { "stringUnit" : { "state" : "translated", @@ -34280,11 +34061,6 @@ "value" : "Version: %1$@ (%2$@)" } }, - "sr" : { - "stringUnit" : { - "state" : "translated", - "value" : "Верзија: %1$@ (%2$@)" - }, "zh-Hant-TW" : { "stringUnit" : { "state" : "translated", @@ -34477,12 +34253,6 @@ "value" : "Volt %@" } }, - "sr" : { - "stringUnit" : { - "state" : "translated", - "value" : "Волти %@" - } - }, "zh-Hant-TW" : { "stringUnit" : { "state" : "translated", @@ -34839,12 +34609,6 @@ "value" : "Peso" } }, - "sr" : { - "stringUnit" : { - "state" : "translated", - "value" : "Тежина" - } - }, "zh-Hant-TW" : { "stringUnit" : { "state" : "translated", @@ -35149,12 +34913,6 @@ "value" : "Vento" } }, - "sr" : { - "stringUnit" : { - "state" : "translated", - "value" : "Ветар" - } - }, "zh-Hant-TW" : { "stringUnit" : { "state" : "translated", @@ -35608,12 +35366,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" : { - "sr" : { - "stringUnit" : { - "state" : "translated", - "value" : "Ваш чвор ће периодично слати нешифровани пакет извештаја мапе на конфигурисани MQTT сервер, што укључује ИД, кратко и дуго име, приближну локацију, модел хардвера, улогу, верзију фирмвера, ЛоРа регион, модем пресет и назив примарног канала." - } - }, "zh-Hant-TW" : { "stringUnit" : { "state" : "translated", diff --git a/Meshtastic/Extensions/UserDefaults.swift b/Meshtastic/Extensions/UserDefaults.swift index ee9e7c21..87bfe3f2 100644 --- a/Meshtastic/Extensions/UserDefaults.swift +++ b/Meshtastic/Extensions/UserDefaults.swift @@ -19,11 +19,11 @@ struct UserDefault { var wrappedValue: T { get { - if defaultValue is any RawRepresentable { + if defaultValue as? any RawRepresentable != nil { let storedValue = UserDefaults.standard.object(forKey: key.rawValue) guard let storedValue, - let jsonString = (storedValue is String) ? "\"\(storedValue)\"" : "\(storedValue)", + let jsonString = (storedValue as? String != nil) ? "\"\(storedValue)\"" : "\(storedValue)", let data = jsonString.data(using: .utf8), let value = (try? JSONDecoder().decode(T.self, from: data)) else { return defaultValue } diff --git a/Meshtastic/Tips/BluetoothTips.swift b/Meshtastic/Tips/BluetoothTips.swift index a10f15d7..838d29fc 100644 --- a/Meshtastic/Tips/BluetoothTips.swift +++ b/Meshtastic/Tips/BluetoothTips.swift @@ -13,7 +13,7 @@ struct BluetoothConnectionTip: Tip { return "tip.bluetooth.connect" } var title: Text { - Text("connected.radio") + Text("Connected Radio") } var message: Text? { Text("Shows information for the Lora radio connected via bluetooth. You can swipe left to disconnect the radio and long press start the live activity.") diff --git a/Meshtastic/Views/Messages/ChannelMessageList.swift b/Meshtastic/Views/Messages/ChannelMessageList.swift index 1cf8a1b0..458d20ed 100644 --- a/Meshtastic/Views/Messages/ChannelMessageList.swift +++ b/Meshtastic/Views/Messages/ChannelMessageList.swift @@ -51,7 +51,6 @@ struct ChannelMessageList: View { messageToHighlight = messageNum } scrollView.scrollTo(messageNum, anchor: .center) - // Reset highlight after delay Task { try? await Task.sleep(nanoseconds: 1_000_000_000) // 1 second diff --git a/Meshtastic/Views/Messages/UserMessageList.swift b/Meshtastic/Views/Messages/UserMessageList.swift index 5f2d9b59..54ff0b4d 100644 --- a/Meshtastic/Views/Messages/UserMessageList.swift +++ b/Meshtastic/Views/Messages/UserMessageList.swift @@ -26,7 +26,7 @@ struct UserMessageList: View { @State private var gotFirstUnreadMessage: Bool = false @State private var messageToHighlight: Int64 = 0 - + var body: some View { VStack { ScrollViewReader { scrollView in @@ -53,7 +53,7 @@ struct UserMessageList: View { messageToHighlight = messageNum } scrollView.scrollTo(messageNum, anchor: .center) - + // Reset highlight after delay Task { try? await Task.sleep(nanoseconds: 1_000_000_000) // 1 second diff --git a/Meshtastic/Views/Settings/Config/Module/StoreForwardConfig.swift b/Meshtastic/Views/Settings/Config/Module/StoreForwardConfig.swift index e124435a..c49fadf1 100644 --- a/Meshtastic/Views/Settings/Config/Module/StoreForwardConfig.swift +++ b/Meshtastic/Views/Settings/Config/Module/StoreForwardConfig.swift @@ -44,7 +44,7 @@ struct StoreForwardConfig: View { } if enabled { - Section(header: Text("settings")) { + Section(header: Text("Settings")) { Toggle(isOn: $heartbeat) { Label("Send Heartbeat", systemImage: "waveform.path.ecg") Text("Send a heartbeat to advertise the server's presence.") From a0c8c6f4a058f462dc005aa7d89fb1574a07fe17 Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Tue, 20 May 2025 23:38:47 -0700 Subject: [PATCH 053/213] Remove trace route alert now that there is a countdown timer and firmware throttling --- Localizable.xcstrings | 22 ---------------- .../Views/Helpers/LoRaSignalStrength.swift | 26 +++++++++++-------- .../Helpers/Actions/TraceRouteButton.swift | 7 ----- 3 files changed, 15 insertions(+), 40 deletions(-) diff --git a/Localizable.xcstrings b/Localizable.xcstrings index ba8240c5..1ea85731 100644 --- a/Localizable.xcstrings +++ b/Localizable.xcstrings @@ -31130,28 +31130,6 @@ } } }, - "This could take a while. The response will appear in the trace route log for the node it was sent to." : { - "localizations" : { - "it" : { - "stringUnit" : { - "state" : "translated", - "value" : "L'operazione potrebbe richiedere un po' di tempo. La risposta apparirà nel registro delle rotte di tracciamento per il nodo a cui è stata inviata." - } - }, - "sr" : { - "stringUnit" : { - "state" : "translated", - "value" : "Ово може потрајати. Одговор ће се појавити у евиденцији трасе праћења за чвор којем је послат." - } - }, - "zh-Hant-TW" : { - "stringUnit" : { - "state" : "translated", - "value" : "這可能需要一段時間。回應將會顯示在被發送節點的路由追蹤紀錄中。" - } - } - } - }, "This device will send out range test messages on the selected interval." : { "localizations" : { "it" : { diff --git a/Meshtastic/Views/Helpers/LoRaSignalStrength.swift b/Meshtastic/Views/Helpers/LoRaSignalStrength.swift index c207d084..b58e8987 100644 --- a/Meshtastic/Views/Helpers/LoRaSignalStrength.swift +++ b/Meshtastic/Views/Helpers/LoRaSignalStrength.swift @@ -51,6 +51,21 @@ struct LoRaSignalStrengthMeter_Previews: PreviewProvider { static var previews: some View { ScrollView { VStack { + VStack { + // Good + LoRaSignalStrengthMeter(snr: -10, rssi: -100, preset: ModemPresets.longFast, compact: true) + .padding(.bottom) + // Fair + LoRaSignalStrengthMeter(snr: -9.5, rssi: -119, preset: ModemPresets.longFast, compact: true) + .padding(.bottom) + // Bad + LoRaSignalStrengthMeter(snr: -12.75, rssi: -139, preset: ModemPresets.longFast, compact: true) + .padding(.bottom) + // None + LoRaSignalStrengthMeter(snr: -26.0, rssi: -128, preset: ModemPresets.longFast, compact: true) + .padding(.bottom) + }.padding() + HStack { // Good LoRaSignalStrengthMeter(snr: -1, rssi: -114, preset: ModemPresets.longFast, compact: false) @@ -85,16 +100,5 @@ struct LoRaSignalStrengthMeter_Previews: PreviewProvider { } .padding(.top) } - - VStack { - // Good - LoRaSignalStrengthMeter(snr: -10, rssi: -100, preset: ModemPresets.longFast, compact: true) - // Fair - LoRaSignalStrengthMeter(snr: -9.5, rssi: -119, preset: ModemPresets.longFast, compact: true) - // Bad - LoRaSignalStrengthMeter(snr: -12.75, rssi: -139, preset: ModemPresets.longFast, compact: true) - // None - LoRaSignalStrengthMeter(snr: -26.0, rssi: -128, preset: ModemPresets.longFast, compact: true) - } } } diff --git a/Meshtastic/Views/Nodes/Helpers/Actions/TraceRouteButton.swift b/Meshtastic/Views/Nodes/Helpers/Actions/TraceRouteButton.swift index fe5d8c87..95489e95 100644 --- a/Meshtastic/Views/Nodes/Helpers/Actions/TraceRouteButton.swift +++ b/Meshtastic/Views/Nodes/Helpers/Actions/TraceRouteButton.swift @@ -31,13 +31,6 @@ struct TraceRouteButton: View { .symbolRenderingMode(.hierarchical) } } - }.alert( - "Trace Route Sent", - isPresented: $isPresentingTraceRouteSentAlert - ) { - Button("OK") { }.keyboardShortcut(.defaultAction) - } message: { - Text("This could take a while. The response will appear in the trace route log for the node it was sent to.") } } } From 530872e50f238fbbc142752ab2ab07867f9d2fde Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Wed, 21 May 2025 11:48:47 -0700 Subject: [PATCH 054/213] capitilize node list, update unmessagable logic, clean up contact list filtering --- Meshtastic.xcodeproj/project.pbxproj | 4 +- Meshtastic/Helpers/MeshPackets.swift | 49 ++-- .../contents | 2 +- Meshtastic/Persistence/UpdateCoreData.swift | 18 +- Meshtastic/Views/Messages/UserList.swift | 243 +++++++++--------- .../Nodes/Helpers/Map/PositionPopover.swift | 2 +- 6 files changed, 168 insertions(+), 150 deletions(-) diff --git a/Meshtastic.xcodeproj/project.pbxproj b/Meshtastic.xcodeproj/project.pbxproj index 6271830c..67778f10 100644 --- a/Meshtastic.xcodeproj/project.pbxproj +++ b/Meshtastic.xcodeproj/project.pbxproj @@ -66,7 +66,7 @@ BCB613852C68703800485544 /* NodePositionIntent.swift in Sources */ = {isa = PBXBuildFile; fileRef = BCB613842C68703800485544 /* NodePositionIntent.swift */; }; BCB613872C69A0FB00485544 /* AppIntentErrors.swift in Sources */ = {isa = PBXBuildFile; fileRef = BCB613862C69A0FB00485544 /* AppIntentErrors.swift */; }; BCD93CBA2D9E11A2006C9214 /* DisconnectNodeIntent.swift in Sources */ = {isa = PBXBuildFile; fileRef = BCD93CB92D9E11A2006C9214 /* DisconnectNodeIntent.swift */; }; - BCDDFA9A2DBB180D0065189C /* ScrollToBottomButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = BCDDFA992DBB180D0065189C /* ScrollToBottomButton.swift */; }; + BCDDFA9A2DBB180D0065189C /* ScrollToBottomButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = BCDDFA992DBB180D0065189C /* ScrollToBottomButton.swift */; }; BCE2D3C32C7ADF42008E6199 /* ShutDownNodeIntent.swift in Sources */ = {isa = PBXBuildFile; fileRef = BCE2D3C22C7ADF42008E6199 /* ShutDownNodeIntent.swift */; }; BCE2D3C52C7AE369008E6199 /* RestartNodeIntent.swift in Sources */ = {isa = PBXBuildFile; fileRef = BCE2D3C42C7AE369008E6199 /* RestartNodeIntent.swift */; }; BCE2D3C72C7B0D0A008E6199 /* ShortcutsProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = BCE2D3C62C7B0D0A008E6199 /* ShortcutsProvider.swift */; }; @@ -333,7 +333,7 @@ BCB613842C68703800485544 /* NodePositionIntent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NodePositionIntent.swift; sourceTree = ""; }; BCB613862C69A0FB00485544 /* AppIntentErrors.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppIntentErrors.swift; sourceTree = ""; }; BCD93CB92D9E11A2006C9214 /* DisconnectNodeIntent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DisconnectNodeIntent.swift; sourceTree = ""; }; - BCDDFA992DBB180D0065189C /* ScrollToBottomButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ScrollToBottomButton.swift; sourceTree = ""; }; + BCDDFA992DBB180D0065189C /* ScrollToBottomButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ScrollToBottomButton.swift; sourceTree = ""; }; BCE2D3C22C7ADF42008E6199 /* ShutDownNodeIntent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShutDownNodeIntent.swift; sourceTree = ""; }; BCE2D3C42C7AE369008E6199 /* RestartNodeIntent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RestartNodeIntent.swift; sourceTree = ""; }; BCE2D3C62C7B0D0A008E6199 /* ShortcutsProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShortcutsProvider.swift; sourceTree = ""; }; diff --git a/Meshtastic/Helpers/MeshPackets.swift b/Meshtastic/Helpers/MeshPackets.swift index e0870401..fbf64ab9 100644 --- a/Meshtastic/Helpers/MeshPackets.swift +++ b/Meshtastic/Helpers/MeshPackets.swift @@ -317,12 +317,17 @@ func nodeInfoPacket (nodeInfo: NodeInfo, channel: UInt32, context: NSManagedObje newUser.pkiEncrypted = true newUser.publicKey = nodeInfo.user.publicKey } - let roles: [Int32] = [2, 4, 5, 6, 7, 10, 11] - if roles.contains(Int32(newUser.role)) { - newUser.unmessagable = true + /// For nodes that have the optional isUnmessagable boolean use that, otherwise excluded roles that are unmessagable by default + if nodeInfo.user.hasIsUnmessagable { + newUser.unmessagable = nodeInfo.user.isUnmessagable } else { - newUser.unmessagable = false - } + let roles = [2, 4, 5, 6, 7, 10, 11] + let containsRole = roles.contains(Int(newUser.role)) + if containsRole { + newUser.unmessagable = true + } else { + newUser.unmessagable = false + }} newNode.user = newUser } else if nodeInfo.num > Constants.minimumNodeNum { let newUser = createUser(num: Int64(nodeInfo.num), context: context) @@ -386,25 +391,31 @@ func nodeInfoPacket (nodeInfo: NodeInfo, channel: UInt32, context: NSManagedObje fetchedNode[0].user?.pkiEncrypted = true fetchedNode[0].user?.publicKey = nodeInfo.user.publicKey } - fetchedNode[0].user!.userId = nodeInfo.user.id - fetchedNode[0].user!.num = Int64(nodeInfo.num) - fetchedNode[0].user!.numString = String(nodeInfo.num) - fetchedNode[0].user!.longName = nodeInfo.user.longName - fetchedNode[0].user!.shortName = nodeInfo.user.shortName - fetchedNode[0].user!.isLicensed = nodeInfo.user.isLicensed - fetchedNode[0].user!.role = Int32(nodeInfo.user.role.rawValue) - fetchedNode[0].user!.hwModel = String(describing: nodeInfo.user.hwModel).uppercased() - fetchedNode[0].user!.hwModelId = Int32(nodeInfo.user.hwModel.rawValue) - let roles: [Int32] = [-1, 2, 4, 5, 6, 7, 10, 11] - if roles.contains(Int32(fetchedNode[0].user?.role ?? -1)) { - fetchedNode[0].user!.unmessagable = true + fetchedNode[0].user?.userId = nodeInfo.user.id + fetchedNode[0].user?.num = Int64(nodeInfo.num) + fetchedNode[0].user?.numString = String(nodeInfo.num) + fetchedNode[0].user?.longName = nodeInfo.user.longName + fetchedNode[0].user?.shortName = nodeInfo.user.shortName + fetchedNode[0].user?.isLicensed = nodeInfo.user.isLicensed + fetchedNode[0].user?.role = Int32(nodeInfo.user.role.rawValue) + fetchedNode[0].user?.hwModel = String(describing: nodeInfo.user.hwModel).uppercased() + fetchedNode[0].user?.hwModelId = Int32(nodeInfo.user.hwModel.rawValue) + /// For nodes that have the optional isUnmessagable boolean use that, otherwise excluded roles that are unmessagable by default + if nodeInfo.user.hasIsUnmessagable { + fetchedNode[0].user?.unmessagable = nodeInfo.user.isUnmessagable } else { - fetchedNode[0].user!.unmessagable = false + let roles = [-1, 2, 4, 5, 6, 7, 10, 11] + let containsRole = roles.contains(Int(fetchedNode[0].user?.role ?? -1)) + if containsRole { + fetchedNode[0].user?.unmessagable = true + } else { + fetchedNode[0].user?.unmessagable = false + } } Task { Api().loadDeviceHardwareData { (hw) in let dh = hw.first(where: { $0.hwModel == fetchedNode[0].user!.hwModelId }) - fetchedNode[0].user!.hwDisplayName = dh?.displayName + fetchedNode[0].user?.hwDisplayName = dh?.displayName } } } else { diff --git a/Meshtastic/Meshtastic.xcdatamodeld/MeshtasticDataModelV 51.xcdatamodel/contents b/Meshtastic/Meshtastic.xcdatamodeld/MeshtasticDataModelV 51.xcdatamodel/contents index f9a2cddb..f6d43c72 100644 --- a/Meshtastic/Meshtastic.xcdatamodeld/MeshtasticDataModelV 51.xcdatamodel/contents +++ b/Meshtastic/Meshtastic.xcdatamodeld/MeshtasticDataModelV 51.xcdatamodel/contents @@ -479,7 +479,7 @@ - + diff --git a/Meshtastic/Persistence/UpdateCoreData.swift b/Meshtastic/Persistence/UpdateCoreData.swift index 2daa8d2b..53da7355 100644 --- a/Meshtastic/Persistence/UpdateCoreData.swift +++ b/Meshtastic/Persistence/UpdateCoreData.swift @@ -183,12 +183,13 @@ func upsertNodeInfoPacket (packet: MeshPacket, context: NSManagedObjectContext) newUser.role = Int32(newUserMessage.role.rawValue) newUser.hwModel = String(describing: newUserMessage.hwModel).uppercased() newUser.hwModelId = Int32(newUserMessage.hwModel.rawValue) + /// For nodes that have the optional isUnmessagable boolean use that, otherwise excluded roles that are unmessagable by default if newUserMessage.hasIsUnmessagable { newUser.unmessagable = newUserMessage.isUnmessagable } else { - // For older firmare make Repeater, Router, Router Late, Sensor, Tracker, TAK, and TAK Tracker unmessagable - let roles: [Int32] = [2, 4, 5, 6, 7, 10, 11] - if roles.contains(Int32(newUser.role)) { + let roles = [2, 4, 5, 6, 7, 10, 11] + let containsRole = roles.contains(Int(newUser.role)) + if containsRole { newUser.unmessagable = true } else { newUser.unmessagable = false @@ -290,15 +291,16 @@ func upsertNodeInfoPacket (packet: MeshPacket, context: NSManagedObjectContext) fetchedNode[0].user!.role = Int32(nodeInfoMessage.user.role.rawValue) fetchedNode[0].user!.hwModel = String(describing: nodeInfoMessage.user.hwModel).uppercased() fetchedNode[0].user!.hwModelId = Int32(nodeInfoMessage.user.hwModel.rawValue) + /// For nodes that have the optional isUnmessagable boolean use that, otherwise excluded roles that are unmessagable by default if nodeInfoMessage.user.hasIsUnmessagable { fetchedNode[0].user!.unmessagable = nodeInfoMessage.user.isUnmessagable } else { - // For older firmare make Repeater, Router, Router Late, Sensor, Tracker, TAK, and TAK Tracker unmessagable - let roles: [Int32] = [-1, 2, 4, 5, 6, 7, 10, 11] - if roles.contains(Int32(fetchedNode[0].user?.role ?? -1)) { - fetchedNode[0].user!.unmessagable = true + let roles = [-1, 2, 4, 5, 6, 7, 10, 11] + let containsRole = roles.contains(Int(fetchedNode[0].user?.role ?? -1)) + if containsRole { + fetchedNode[0].user?.unmessagable = true } else { - fetchedNode[0].user!.unmessagable = false + fetchedNode[0].user?.unmessagable = false } } if !nodeInfoMessage.user.publicKey.isEmpty { diff --git a/Meshtastic/Views/Messages/UserList.swift b/Meshtastic/Views/Messages/UserList.swift index ddb1264e..a74e3fd2 100644 --- a/Meshtastic/Views/Messages/UserList.swift +++ b/Meshtastic/Views/Messages/UserList.swift @@ -21,6 +21,7 @@ struct UserList: View { @State private var isPkiEncrypted = false @State private var isFavorite = false @State private var isIgnored = false + @State private var isUnmessagable = false @State private var isEnvironment = false @State private var distanceFilter = false @State private var maxDistance: Double = 800000 @@ -46,8 +47,9 @@ struct UserList: View { NSSortDescriptor(key: "userNode.lastHeard", ascending: false), NSSortDescriptor(key: "longName", ascending: true)], predicate: NSPredicate( - format: "userNode.ignored == false && longName != '' AND unmessagable == false" - ), animation: .default) + format: "userNode.ignored == NO AND unmessagable = NO" + ), animation: .spring + ) var users: FetchedResults @Binding var node: NodeInfoEntity? @@ -59,141 +61,139 @@ struct UserList: View { let localeDateFormat = DateFormatter.dateFormat(fromTemplate: "yyMMdd", options: 0, locale: Locale.current) let dateFormatString = (localeDateFormat ?? "MM/dd/YY") VStack { - List(selection: $userSelection) { - ForEach(users) { (user: UserEntity) in - let mostRecent = user.messageList.last - 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 - if user.num != bleManager.connectedPeripheral?.num ?? 0 { - NavigationLink(value: user) { - ZStack { - Image(systemName: "circle.fill") - .opacity(user.unreadMessages > 0 ? 1 : 0) - .font(.system(size: 10)) - .foregroundColor(.accentColor) - .brightness(0.2) - } + List(users, selection: $userSelection) { (user: UserEntity) in + let mostRecent = user.messageList.last + 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 + if user.num != bleManager.connectedPeripheral?.num ?? 0 { + NavigationLink(value: user) { + ZStack { + Image(systemName: "circle.fill") + .opacity(user.unreadMessages > 0 ? 1 : 0) + .font(.system(size: 10)) + .foregroundColor(.accentColor) + .brightness(0.2) + } - CircleText(text: user.shortName ?? "?", color: Color(UIColor(hex: UInt32(user.num)))) + CircleText(text: user.shortName ?? "?", color: Color(UIColor(hex: UInt32(user.num)))) - VStack(alignment: .leading) { - HStack { - if user.pkiEncrypted { - if !user.keyMatch { - /// Public Key on the User and the Public Key on the Last Message don't match - Image(systemName: "key.slash") - .foregroundColor(.red) - } else { - Image(systemName: "lock.fill") - .foregroundColor(.green) - } + VStack(alignment: .leading) { + HStack { + if user.pkiEncrypted { + if !user.keyMatch { + /// Public Key on the User and the Public Key on the Last Message don't match + Image(systemName: "key.slash") + .foregroundColor(.red) } else { - Image(systemName: "lock.open.fill") - .foregroundColor(.yellow) - } - Text(user.longName ?? "Unknown".localized) - .font(.headline) - .allowsTightening(true) - Spacer() - if user.userNode?.favorite ?? false { - Image(systemName: "star.fill") - .foregroundColor(.yellow) - } - if user.messageList.count > 0 { - if lastMessageDay == currentDay { - Text(lastMessageTime, style: .time ) - .font(.footnote) - .foregroundColor(.secondary) - } else if lastMessageDay == (currentDay - 1) { - Text("Yesterday") - .font(.footnote) - .foregroundColor(.secondary) - } else if lastMessageDay < (currentDay - 1) && lastMessageDay > (currentDay - 5) { - Text(lastMessageTime.formattedDate(format: dateFormatString)) - .font(.footnote) - .foregroundColor(.secondary) - } else if lastMessageDay < (currentDay - 1800) { - Text(lastMessageTime.formattedDate(format: dateFormatString)) - .font(.footnote) - .foregroundColor(.secondary) - } + Image(systemName: "lock.fill") + .foregroundColor(.green) } + } else { + Image(systemName: "lock.open.fill") + .foregroundColor(.yellow) + } + Text(user.longName ?? "Unknown".localized) + .font(.headline) + .allowsTightening(true) + Spacer() + if user.userNode?.favorite ?? false { + Image(systemName: "star.fill") + .foregroundColor(.yellow) } - if user.messageList.count > 0 { - HStack(alignment: .top) { - Text("\(mostRecent != nil ? mostRecent!.messagePayload! : " ")") + if lastMessageDay == currentDay { + Text(lastMessageTime, style: .time ) + .font(.footnote) + .foregroundColor(.secondary) + } else if lastMessageDay == (currentDay - 1) { + Text("Yesterday") + .font(.footnote) + .foregroundColor(.secondary) + } else if lastMessageDay < (currentDay - 1) && lastMessageDay > (currentDay - 5) { + Text(lastMessageTime.formattedDate(format: dateFormatString)) + .font(.footnote) + .foregroundColor(.secondary) + } else if lastMessageDay < (currentDay - 1800) { + Text(lastMessageTime.formattedDate(format: dateFormatString)) .font(.footnote) .foregroundColor(.secondary) } } } - } - .frame(height: 62) - .contextMenu { - Button { - if node != nil && !(user.userNode?.favorite ?? false) { - let success = bleManager.setFavoriteNode(node: user.userNode!, connectedNodeNum: Int64(node!.num)) - if success { - user.userNode?.favorite = !(user.userNode?.favorite ?? true) - Logger.data.info("Favorited a node") - } - } else { - let success = bleManager.removeFavoriteNode(node: user.userNode!, connectedNodeNum: Int64(node!.num)) - if success { - user.userNode?.favorite = !(user.userNode?.favorite ?? true) - Logger.data.info("Un Favorited a node") - } - } - context.refresh(user, mergeChanges: true) - do { - try context.save() - } catch { - context.rollback() - Logger.data.error("Save Node Favorite Error") - } - } label: { - Label((user.userNode?.favorite ?? false) ? "Un-Favorite" : "Favorite", systemImage: (user.userNode?.favorite ?? false) ? "star.slash.fill" : "star.fill") - } - Button { - user.mute = !user.mute - do { - try context.save() - } catch { - context.rollback() - Logger.data.error("Save User Mute Error") - } - } label: { - Label(user.mute ? "Show Alerts" : "Hide Alerts", systemImage: user.mute ? "bell" : "bell.slash") - } - if user.messageList.count > 0 { - Button(role: .destructive) { - isPresentingDeleteUserMessagesConfirm = true - userSelection = user - } label: { - Label("Delete Messages", systemImage: "trash") + if user.messageList.count > 0 { + HStack(alignment: .top) { + Text("\(mostRecent != nil ? mostRecent!.messagePayload! : " ")") + .font(.footnote) + .foregroundColor(.secondary) } } } - .confirmationDialog( - "This conversation will be deleted.", - isPresented: $isPresentingDeleteUserMessagesConfirm, - titleVisibility: .visible - ) { - Button(role: .destructive) { - deleteUserMessages(user: userSelection!, context: context) - context.refresh(node!.user!, mergeChanges: true) - } label: { - Text("Delete") + } + .frame(height: 62) + .contextMenu { + Button { + + if node != nil && !(user.userNode?.favorite ?? false) { + let success = bleManager.setFavoriteNode(node: user.userNode!, connectedNodeNum: Int64(node!.num)) + if success { + user.userNode?.favorite = !(user.userNode?.favorite ?? true) + Logger.data.info("Favorited a node") + } + } else { + let success = bleManager.removeFavoriteNode(node: user.userNode!, connectedNodeNum: Int64(node!.num)) + if success { + user.userNode?.favorite = !(user.userNode?.favorite ?? true) + Logger.data.info("Un Favorited a node") + } } + context.refresh(user, mergeChanges: true) + do { + try context.save() + } catch { + context.rollback() + Logger.data.error("Save Node Favorite Error") + } + } label: { + Label((user.userNode?.favorite ?? false) ? "Un-Favorite" : "Favorite", systemImage: (user.userNode?.favorite ?? false) ? "star.slash.fill" : "star.fill") + } + Button { + user.mute = !user.mute + do { + try context.save() + } catch { + context.rollback() + Logger.data.error("Save User Mute Error") + } + } label: { + Label(user.mute ? "Show Alerts" : "Hide Alerts", systemImage: user.mute ? "bell" : "bell.slash") + } + if user.messageList.count > 0 { + Button(role: .destructive) { + isPresentingDeleteUserMessagesConfirm = true + userSelection = user + } label: { + Label("Delete Messages", systemImage: "trash") + } + } + } + .confirmationDialog( + "This conversation will be deleted.", + isPresented: $isPresentingDeleteUserMessagesConfirm, + titleVisibility: .visible + ) { + Button(role: .destructive) { + deleteUserMessages(user: userSelection!, context: context) + context.refresh(node!.user!, mergeChanges: true) + } label: { + Text("Delete") } } } } .listStyle(.plain) - .navigationTitle(String.localizedStringWithFormat("Contacts (%@)".localized, String(users.count == 0 ? 0 : users.count))) + .navigationTitle(String.localizedStringWithFormat("Contacts (%@)".localized, String(users.count == 0 ? 0 : users.count - 1))) .sheet(isPresented: $editingFilters) { NodeListFilter(filterTitle: "Contact Filters", viaLora: $viaLora, viaMqtt: $viaMqtt, isOnline: $isOnline, isPkiEncrypted: $isPkiEncrypted, isFavorite: $isFavorite, isIgnored: $isIgnored, isEnvironment: $isEnvironment, distanceFilter: $distanceFilter, maximumDistance: $maxDistance, hopsAway: $hopsAway, roleFilter: $roleFilter, deviceRoles: $deviceRoles) } @@ -246,7 +246,7 @@ struct UserList: View { await searchUserList() } } - .onAppear { + .onFirstAppear { Task { await searchUserList() } @@ -297,8 +297,6 @@ struct UserList: View { let textSearchPredicate = NSCompoundPredicate(type: .or, subpredicates: searchPredicates) /// Create an array of predicates to hold our AND predicates var predicates: [NSPredicate] = [] - let defaultPredicate = NSPredicate(format: "userNode.ignored == NO AND longName != '' AND unmessagable == NO") - predicates.append(defaultPredicate) /// Mqtt and lora if !(viaLora && viaMqtt) { if viaLora { @@ -345,6 +343,13 @@ struct UserList: View { let isFavoritePredicate = NSPredicate(format: "userNode.favorite == YES") predicates.append(isFavoritePredicate) } + /// Ignored + let isIgnoredPredicate = NSPredicate(format: "userNode.ignored == NO") + predicates.append(isIgnoredPredicate) + /// Unmessagable + let isUnmessagablePredicate = NSPredicate(format: "unmessagable == NO") + predicates.append(isUnmessagablePredicate) + /// Distance if distanceFilter { let pointOfInterest = LocationsHandler.currentLocation diff --git a/Meshtastic/Views/Nodes/Helpers/Map/PositionPopover.swift b/Meshtastic/Views/Nodes/Helpers/Map/PositionPopover.swift index d3ef18a3..76e474b8 100644 --- a/Meshtastic/Views/Nodes/Helpers/Map/PositionPopover.swift +++ b/Meshtastic/Views/Nodes/Helpers/Map/PositionPopover.swift @@ -156,7 +156,7 @@ struct PositionPopover: View { if lastLocation.distance(from: CLLocation(latitude: LocationsHandler.DefaultLocation.latitude, longitude: LocationsHandler.DefaultLocation.longitude)) > 0.0 { let metersAway = position.coordinate.distance(from: CLLocationCoordinate2D(latitude: lastLocation.coordinate.latitude, longitude: lastLocation.coordinate.longitude)) Label { - Text("distance".localized + ": \(distanceFormatter.string(fromDistance: Double(metersAway)))") + Text("Distance".localized + ": \(distanceFormatter.string(fromDistance: Double(metersAway)))") .foregroundColor(.primary) .font(idiom == .phone ? .callout : .body) } icon: { From b962c68ac23843f72ac975e9aa2e2b8677a2e607 Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Wed, 21 May 2025 22:11:03 -0700 Subject: [PATCH 055/213] Position precision slider cleanup --- Meshtastic/Views/Settings/Channels/ChannelForm.swift | 10 +++++----- .../Views/Settings/Config/Module/MQTTConfig.swift | 6 +++--- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/Meshtastic/Views/Settings/Channels/ChannelForm.swift b/Meshtastic/Views/Settings/Channels/ChannelForm.swift index 92f1b8e2..acbb11b0 100644 --- a/Meshtastic/Views/Settings/Channels/ChannelForm.swift +++ b/Meshtastic/Views/Settings/Channels/ChannelForm.swift @@ -148,7 +148,7 @@ struct ChannelForm: View { .listRowSeparator(.visible) .onChange(of: preciseLocation) { _, pl in if pl == false { - positionPrecision = 14 + positionPrecision = 15 } } } @@ -157,11 +157,11 @@ struct ChannelForm: View { VStack(alignment: .leading) { Label("Approximate Location", systemImage: "location.slash.circle.fill") - Slider(value: $positionPrecision, in: 11...14, step: 1) { + Slider(value: $positionPrecision, in: 12...15, step: 1) { } minimumValueLabel: { - Image(systemName: "minus") - } maximumValueLabel: { Image(systemName: "plus") + } maximumValueLabel: { + Image(systemName: "minus") } Text(PositionPrecision(rawValue: Int(positionPrecision))?.description ?? "") .foregroundColor(.gray) @@ -228,7 +228,7 @@ struct ChannelForm: View { .onChange(of: positionsEnabled) { _, pe in if pe { if positionPrecision == 0 { - positionPrecision = 14 + positionPrecision = 15 } } else { positionPrecision = 0 diff --git a/Meshtastic/Views/Settings/Config/Module/MQTTConfig.swift b/Meshtastic/Views/Settings/Config/Module/MQTTConfig.swift index bd2dfeeb..5506dde3 100644 --- a/Meshtastic/Views/Settings/Config/Module/MQTTConfig.swift +++ b/Meshtastic/Views/Settings/Config/Module/MQTTConfig.swift @@ -130,11 +130,11 @@ struct MQTTConfig: View { Text("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.") .foregroundColor(.gray) .font(.callout) - Slider(value: $mapPositionPrecision, in: 11...14, step: 1) { + Slider(value: $mapPositionPrecision, in: 12...15, step: 1) { } minimumValueLabel: { - Image(systemName: "minus") - } maximumValueLabel: { Image(systemName: "plus") + } maximumValueLabel: { + Image(systemName: "minus") } Text(PositionPrecision(rawValue: Int(mapPositionPrecision))?.description ?? "") .foregroundColor(.gray) From 9ecfe6a3e7b249dc9248cd6a456d97079670cb0f Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Thu, 22 May 2025 08:19:19 -0700 Subject: [PATCH 056/213] MQTT interval default, remove orphaned code --- Meshtastic/Views/Bluetooth/Connect.swift | 45 +------------------ .../Settings/Config/Module/MQTTConfig.swift | 8 +++- 2 files changed, 7 insertions(+), 46 deletions(-) diff --git a/Meshtastic/Views/Bluetooth/Connect.swift b/Meshtastic/Views/Bluetooth/Connect.swift index 2ff00669..a804a77d 100644 --- a/Meshtastic/Views/Bluetooth/Connect.swift +++ b/Meshtastic/Views/Bluetooth/Connect.swift @@ -26,10 +26,6 @@ struct Connect: View { @State var liveActivityStarted = false @State var presentingSwitchPreferredPeripheral = false @State var selectedPeripherialId = "" - @State private var showSwipeDemo = false - @State private var swipeDemoOffset: CGFloat = 0 - @State private var showDeleteButton: Bool = false - @AppStorage("hasSeenSwipeDemo") private var hasSeenSwipeDemo = false init () { let notificationCenter = UNUserNotificationCenter.current() @@ -93,28 +89,6 @@ struct Connect: View { .font(.caption) .foregroundColor(Color.gray) .padding([.top]) - .offset(x: swipeDemoOffset) - .overlay( - GeometryReader { geometry in - ZStack { - Rectangle() - .foregroundColor(.red) - .frame(width: 80) - .offset(x: geometry.size.width - 80) - VStack { - Image(systemName: "antenna.radiowaves.left.and.right.slash") - .foregroundColor(.white) - .font(.title3) - Text("Disconnect") - .foregroundColor(.white) - .font(.caption) - } - } - .offset(x: geometry.size.width - 50) - - .opacity(showDeleteButton ? 1 : 0) - } - ) .swipeActions { Button(role: .destructive) { if let connectedPeripheral = bleManager.connectedPeripheral, @@ -149,7 +123,7 @@ struct Connect: View { Text("Short Name: \(node?.user?.shortName ?? "?")") Text("Long Name: \(node?.user?.longName?.addingVariationSelectors ?? "Unknown".localized)") Text("BLE RSSI: \(connectedPeripheral.rssi)") - + Button(role: .destructive) { if let connectedPeripheral = bleManager.connectedPeripheral, connectedPeripheral.peripheral.state == .connected { @@ -357,23 +331,6 @@ struct Connect: View { } else { isUnsetRegion = false } - // Show swipe demo if user hasn't seen it before and we're connected - if !hasSeenSwipeDemo && bleManager.isSubscribed && node != nil { - DispatchQueue.main.asyncAfter(deadline: .now() + 1.0) { - withAnimation(.easeInOut(duration: 0.6)) { - swipeDemoOffset = -80 - showDeleteButton = true - } - DispatchQueue.main.asyncAfter(deadline: .now() + 1.5) { - withAnimation(.easeInOut(duration: 0.6)) { - swipeDemoOffset = 0 - showDeleteButton = false - } - // Mark as seen so it doesn't appear again - hasSeenSwipeDemo = true - } - } - } } catch { Logger.data.error("💥 Error fetching node info: \(error.localizedDescription, privacy: .public)") } diff --git a/Meshtastic/Views/Settings/Config/Module/MQTTConfig.swift b/Meshtastic/Views/Settings/Config/Module/MQTTConfig.swift index 5506dde3..f06cf45c 100644 --- a/Meshtastic/Views/Settings/Config/Module/MQTTConfig.swift +++ b/Meshtastic/Views/Settings/Config/Module/MQTTConfig.swift @@ -112,7 +112,6 @@ struct MQTTConfig: View { Label("I have read and understand the above. I voluntarily consent to the unencrypted transmission of my node data via MQTT.", systemImage: "hand.raised") .foregroundColor(.gray) .font(.callout) - } .toggleStyle(SwitchToggleStyle(tint: .accentColor)) } @@ -429,8 +428,13 @@ struct MQTTConfig: View { self.tlsEnabled = node?.mqttConfig?.tlsEnabled ?? false self.mqttConnected = bleManager.mqttProxyConnected self.mapReportingEnabled = node?.mqttConfig?.mapReportingEnabled ?? false - self.mapPublishIntervalSecs = Int(node?.mqttConfig?.mapPublishIntervalSecs ?? 3600) + if node?.mqttConfig?.mapPublishIntervalSecs ?? 0 < 3600 { + self.mapPublishIntervalSecs = 3600 + } else { + self.mapPublishIntervalSecs = Int(node?.mqttConfig?.mapPublishIntervalSecs ?? 3600) + } self.mapPositionPrecision = Double(node?.mqttConfig?.mapPositionPrecision ?? 14) + self.mapReportingOptIn = UserDefaults.mapReportingOptIn if mapPositionPrecision < 11 || mapPositionPrecision > 14 { self.mapPositionPrecision = 14 self.hasChanges = true From 446b94604c98b3a24ba74bee25cc858f311cba58 Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Thu, 22 May 2025 09:50:43 -0700 Subject: [PATCH 057/213] Client proxy cleanup --- Meshtastic/Helpers/Mqtt/MqttClientProxyManager.swift | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/Meshtastic/Helpers/Mqtt/MqttClientProxyManager.swift b/Meshtastic/Helpers/Mqtt/MqttClientProxyManager.swift index cf21d92e..62b9dc4b 100644 --- a/Meshtastic/Helpers/Mqtt/MqttClientProxyManager.swift +++ b/Meshtastic/Helpers/Mqtt/MqttClientProxyManager.swift @@ -41,31 +41,29 @@ class MqttClientProxyManager { if let host = host { let port = defaultServerPort - let username = node.mqttConfig?.username - let password = node.mqttConfig?.password let root = node.mqttConfig?.root?.count ?? 0 > 0 ? node.mqttConfig?.root : "msh" let prefix = root! topic = prefix + "/2/e" + "/#" // Require opt in to map report terms to connect if node.mqttConfig?.mapReportingEnabled ?? false && UserDefaults.mapReportingOptIn || !(node.mqttConfig?.mapReportingEnabled ?? false) { - connect(host: host, port: port, useSsl: useSsl, username: username, password: password, topic: topic) + connect(host: host, port: port, useSsl: useSsl, topic: topic, node: node) } else { delegate?.onMqttError(message: "MQTT Map Reporting Terms need to be accepted.") } } } - func connect(host: String, port: Int, useSsl: Bool, username: String?, password: String?, topic: String?) { + func connect(host: String, port: Int, useSsl: Bool, topic: String?, node: NodeInfoEntity) { guard !host.isEmpty else { delegate?.onMqttDisconnected() return } - let clientId = "MeshtasticAppleMqttProxy-" + String(ProcessInfo().processIdentifier) + let clientId = "MeshtasticAppleMqttProxy-" + (node.user?.userId ?? String(ProcessInfo().processIdentifier)) mqttClientProxy = CocoaMQTT(clientID: clientId, host: host, port: UInt16(port)) if let mqttClient = mqttClientProxy { mqttClient.enableSSL = useSsl mqttClient.allowUntrustCACertificate = true - mqttClient.username = username - mqttClient.password = password + mqttClient.username = node.mqttConfig?.username + mqttClient.password = node.mqttConfig?.password mqttClient.keepAlive = 60 mqttClient.cleanSession = true if debugLog { From d40a4f094286b42822d2384123bb6b3bb40ee54d Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Thu, 22 May 2025 11:46:06 -0700 Subject: [PATCH 058/213] Only draw precision circles for the sizes that will be implemented in the firmware --- .../Views/Nodes/Helpers/Map/MapContent/MeshMapContent.swift | 2 +- .../Views/Nodes/Helpers/Map/MapContent/NodeMapContent.swift | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Meshtastic/Views/Nodes/Helpers/Map/MapContent/MeshMapContent.swift b/Meshtastic/Views/Nodes/Helpers/Map/MapContent/MeshMapContent.swift index 4bce572c..f8829d21 100644 --- a/Meshtastic/Views/Nodes/Helpers/Map/MapContent/MeshMapContent.swift +++ b/Meshtastic/Views/Nodes/Helpers/Map/MapContent/MeshMapContent.swift @@ -130,7 +130,7 @@ struct MeshMapContent: MapContent { } } /// Reduced Precision Map Circles - if 10...19 ~= position.precisionBits { + if 12...15 ~= position.precisionBits { let pp = PositionPrecision(rawValue: Int(position.precisionBits)) let radius: CLLocationDistance = pp?.precisionMeters ?? 0 if radius > 0.0 { diff --git a/Meshtastic/Views/Nodes/Helpers/Map/MapContent/NodeMapContent.swift b/Meshtastic/Views/Nodes/Helpers/Map/MapContent/NodeMapContent.swift index 2d157979..88d217ae 100644 --- a/Meshtastic/Views/Nodes/Helpers/Map/MapContent/NodeMapContent.swift +++ b/Meshtastic/Views/Nodes/Helpers/Map/MapContent/NodeMapContent.swift @@ -49,7 +49,7 @@ struct NodeMapContent: MapContent { let pf = PositionFlags(rawValue: Int(position.nodePosition?.metadata?.positionFlags ?? 771)) let headingDegrees = Angle.degrees(Double(position.heading)) /// Reduced Precision Map Circle - if position.latest && 10...19 ~= position.precisionBits { + if position.latest && 12...15 ~= position.precisionBits { let pp = PositionPrecision(rawValue: Int(position.precisionBits)) let radius: CLLocationDistance = pp?.precisionMeters ?? 0 if radius > 0.0 { From 9050bf952398a77fb24ce6f677774cd1e57b59bd Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Fri, 23 May 2025 20:17:26 -0700 Subject: [PATCH 059/213] unmessagable display on node list and details --- Localizable.xcstrings | 3 +++ Meshtastic/Views/Nodes/Helpers/NodeDetail.swift | 13 +++++++++++++ Meshtastic/Views/Nodes/Helpers/NodeListItem.swift | 5 +++++ 3 files changed, 21 insertions(+) diff --git a/Localizable.xcstrings b/Localizable.xcstrings index 1ea85731..e4a2d35b 100644 --- a/Localizable.xcstrings +++ b/Localizable.xcstrings @@ -32899,6 +32899,9 @@ } } } + }, + "Unmonitored or Infrastructure" : { + }, "Unset" : { "localizations" : { diff --git a/Meshtastic/Views/Nodes/Helpers/NodeDetail.swift b/Meshtastic/Views/Nodes/Helpers/NodeDetail.swift index c5670e06..853387e5 100644 --- a/Meshtastic/Views/Nodes/Helpers/NodeDetail.swift +++ b/Meshtastic/Views/Nodes/Helpers/NodeDetail.swift @@ -151,6 +151,19 @@ struct NodeDetail: View { } .accessibilityElement(children: .combine) } + if node.user?.unmessagable ?? false { + HStack { + Label { + Text("") + } icon: { + Image(systemName: "iphone.slash") + .symbolRenderingMode(.multicolor) + } + Spacer() + Text("Unmonitored or Infrastructure") + } + .accessibilityElement(children: .combine) + } if let dm = node.telemetries?.filtered(using: NSPredicate(format: "metricsType == 0")).lastObject as? TelemetryEntity, let uptimeSeconds = dm.uptimeSeconds { HStack { diff --git a/Meshtastic/Views/Nodes/Helpers/NodeListItem.swift b/Meshtastic/Views/Nodes/Helpers/NodeListItem.swift index e7d00a6a..1ceb7583 100644 --- a/Meshtastic/Views/Nodes/Helpers/NodeListItem.swift +++ b/Meshtastic/Views/Nodes/Helpers/NodeListItem.swift @@ -172,6 +172,11 @@ struct NodeListItem: View { let role = DeviceRoles(rawValue: Int(node.user?.role ?? 0)) IconAndText(systemName: role?.systemName ?? "figure", text: "Role: \(role?.name ?? "Unknown".localized)") + if node.user?.unmessagable ?? false { + IconAndText(systemName: "iphone.slash", + renderingMode: .multicolor, + text: "Unmonitored") + } if node.isStoreForwardRouter { IconAndText(systemName: "envelope.arrow.triangle.branch", renderingMode: .multicolor, From 8e198577e98d5be3ab41b546db1d9b866395f11c Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Fri, 23 May 2025 20:32:56 -0700 Subject: [PATCH 060/213] Save isUnmessagable --- Localizable.xcstrings | 6 ++++++ Meshtastic/Views/Settings/UserConfig.swift | 11 +++++++++++ 2 files changed, 17 insertions(+) diff --git a/Localizable.xcstrings b/Localizable.xcstrings index e4a2d35b..e862cdbf 100644 --- a/Localizable.xcstrings +++ b/Localizable.xcstrings @@ -32899,6 +32899,9 @@ } } } + }, + "Unmessagable" : { + }, "Unmonitored or Infrastructure" : { @@ -33498,6 +33501,9 @@ } } } + }, + "Used to identify unmonitored or infrastructure nodes so that messaging is not avaliable to nodes that will never respond." : { + }, "User" : { "localizations" : { diff --git a/Meshtastic/Views/Settings/UserConfig.swift b/Meshtastic/Views/Settings/UserConfig.swift index 4a47255c..f2e5d394 100644 --- a/Meshtastic/Views/Settings/UserConfig.swift +++ b/Meshtastic/Views/Settings/UserConfig.swift @@ -25,6 +25,7 @@ struct UserConfig: View { @State var hasChanges = false @State var shortName = "" @State var longName: String = "" + @State var isUnmessagable: Bool = false @State var isLicensed = false @State var overrideDutyCycle = false @State var overrideFrequency: Float = 0.0 @@ -96,6 +97,13 @@ struct UserConfig: View { Text("The last 4 of the device MAC address will be appended to the short name to set the device's BLE Name. Short name can be up to 4 bytes long.") .foregroundColor(.gray) .font(.callout) + + Toggle(isOn: $isUnmessagable) { + Label("Unmessagable", systemImage: "iphone.slash") + Text("Used to identify unmonitored or infrastructure nodes so that messaging is not avaliable to nodes that will never respond.") + .font(.caption2) + } + .toggleStyle(SwitchToggleStyle(tint: .accentColor)) } // Only manage ham mode for the locally connected node if node?.num ?? 0 > 0 && node?.num ?? 0 == bleManager.connectedPeripheral?.num ?? 0 { @@ -166,6 +174,7 @@ struct UserConfig: View { var u = User() u.shortName = shortName u.longName = longName + u.isUnmessagable = isUnmessagable let adminMessageId = bleManager.saveUser(config: u, fromUser: connectedUser, toUser: node!.user!, adminIndex: connectedNode?.myInfo?.adminIndex ?? 0) if adminMessageId > 0 { hasChanges = false @@ -174,6 +183,7 @@ struct UserConfig: View { } else { var ham = HamParameters() ham.shortName = shortName + //ham.isUnmessagable = isUnmessagable ham.callSign = longName ham.txPower = Int32(txPower) ham.frequency = overrideFrequency @@ -199,6 +209,7 @@ struct UserConfig: View { .onAppear { self.shortName = node?.user?.shortName ?? "" self.longName = node?.user?.longName ?? "" + self.isUnmessagable = node?.user?.unmessagable ?? false self.isLicensed = node?.user?.isLicensed ?? false self.txPower = Int(node?.loRaConfig?.txPower ?? 0) self.overrideFrequency = node?.loRaConfig?.overrideFrequency ?? 0.00 From 350678c2b99e71101c93ace6acb90c4243d7493d Mon Sep 17 00:00:00 2001 From: Benjamin Faershtein <119711889+RCGV1@users.noreply.github.com> Date: Fri, 23 May 2025 20:53:31 -0700 Subject: [PATCH 061/213] Added Favorites Only Map Option --- Localizable.xcstrings | 3 +++ Meshtastic/Extensions/UserDefaults.swift | 4 ++++ .../Map/MapContent/MeshMapContent.swift | 21 ++++++++++++++----- .../Nodes/Helpers/Map/MapSettingsForm.swift | 11 +++++++++- 4 files changed, 33 insertions(+), 6 deletions(-) diff --git a/Localizable.xcstrings b/Localizable.xcstrings index 1ea85731..078e657c 100644 --- a/Localizable.xcstrings +++ b/Localizable.xcstrings @@ -20686,6 +20686,9 @@ } } } + }, + "Only Show Favorites" : { + }, "Open Location Code (aka Plus Codes)" : { "localizations" : { diff --git a/Meshtastic/Extensions/UserDefaults.swift b/Meshtastic/Extensions/UserDefaults.swift index 87bfe3f2..ee9a9ebc 100644 --- a/Meshtastic/Extensions/UserDefaults.swift +++ b/Meshtastic/Extensions/UserDefaults.swift @@ -57,6 +57,7 @@ extension UserDefaults { case enableMapTraffic case enableMapPointsOfInterest case enableOfflineMaps + case onlyShowFavoriteNodesMap case mapTileServer case enableOverlayServer case mapOverlayServer @@ -118,6 +119,9 @@ extension UserDefaults { @UserDefault(.enableMapPointsOfInterest, defaultValue: false) static var enableMapPointsOfInterest: Bool + + @UserDefault(.onlyShowFavoriteNodesMap, defaultValue: false) + static var onlyShowFavoriteNodesMap: Bool @UserDefault(.enableDetectionNotifications, defaultValue: false) static var enableDetectionNotifications: Bool diff --git a/Meshtastic/Views/Nodes/Helpers/Map/MapContent/MeshMapContent.swift b/Meshtastic/Views/Nodes/Helpers/Map/MapContent/MeshMapContent.swift index f8829d21..ba29cc1a 100644 --- a/Meshtastic/Views/Nodes/Helpers/Map/MapContent/MeshMapContent.swift +++ b/Meshtastic/Views/Nodes/Helpers/Map/MapContent/MeshMapContent.swift @@ -15,6 +15,7 @@ struct MeshMapContent: MapContent { @AppStorage("meshMapShowNodeHistory") private var showNodeHistory = false @AppStorage("meshMapShowRouteLines") private var showRouteLines = false @AppStorage("enableMapConvexHull") private var showConvexHull = false + @AppStorage("onlyShowFavoriteNodesMap") private var favoriteNodesOnly = false @Binding var showTraffic: Bool @Binding var showPointsOfInterest: Bool @Binding var selectedMapLayer: MapLayer @@ -39,11 +40,12 @@ struct MeshMapContent: MapContent { @MapContentBuilder var positionAnnotations: some MapContent { ForEach(positions, id: \.id) { position in - /// Node color from node.num - let nodeColor = UIColor(hex: UInt32(position.nodePosition?.num ?? 0)) - let positionName = position.nodePosition?.user?.longName ?? "?" - /// Latest Position Anotations - Annotation(positionName, coordinate: position.coordinate) { + if !favoriteNodesOnly || (position.nodePosition?.favorite == true) { + /// Node color from node.num + let nodeColor = UIColor(hex: UInt32(position.nodePosition?.num ?? 0)) + let positionName = position.nodePosition?.user?.longName ?? "?" + /// Latest Position Anotations + Annotation(positionName, coordinate: position.coordinate) { LazyVStack { ZStack { let nodeColor = UIColor(hex: UInt32(position.nodePosition?.num ?? 0)) @@ -59,6 +61,13 @@ struct MeshMapContent: MapContent { .onAppear { self.scale = 1 } + .onChange(of: favoriteNodesOnly) { + + scale = 0.5 // Reset to initial state + DispatchQueue.main.asyncAfter(deadline: .now() + 0.01) { + scale = 1 + } + } .frame(width: 60, height: 60) } if position.nodePosition?.hasDetectionSensorMetrics ?? false { @@ -141,6 +150,8 @@ struct MeshMapContent: MapContent { } } } + + } } @MapContentBuilder diff --git a/Meshtastic/Views/Nodes/Helpers/Map/MapSettingsForm.swift b/Meshtastic/Views/Nodes/Helpers/Map/MapSettingsForm.swift index 85db444a..acea5d19 100644 --- a/Meshtastic/Views/Nodes/Helpers/Map/MapSettingsForm.swift +++ b/Meshtastic/Views/Nodes/Helpers/Map/MapSettingsForm.swift @@ -15,6 +15,7 @@ struct MapSettingsForm: View { @AppStorage("meshMapShowRouteLines") private var routeLines = false @AppStorage("enableMapConvexHull") private var convexHull = false @AppStorage("enableMapWaypoints") private var waypoints = true + @AppStorage("onlyShowFavoriteNodesMap") private var favoriteNodesOnly = false @Binding var traffic: Bool @Binding var pointsOfInterest: Bool @Binding var mapLayer: MapLayer @@ -61,7 +62,15 @@ struct MapSettingsForm: View { UserDefaults.enableMapWaypoints = !waypoints } } - + + Toggle(isOn: $favoriteNodesOnly) { + Label("Only Show Favorites", systemImage: "star.fill") + } + .toggleStyle(SwitchToggleStyle(tint: .accentColor)) + .onTapGesture { + self.favoriteNodesOnly.toggle() + UserDefaults.onlyShowFavoriteNodesMap = self.favoriteNodesOnly + } Toggle(isOn: $nodeHistory) { Label("Node History", systemImage: "building.columns.fill") } From 18ef8890cbf32e7810875e87b329517883180a8e Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Fri, 23 May 2025 23:13:12 -0700 Subject: [PATCH 062/213] add back notifications delay, disable unmessagable toggle unless it is a supported version --- Meshtastic/Helpers/LocalNotificationManager.swift | 4 ++-- Meshtastic/Views/Settings/UserConfig.swift | 5 +++-- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/Meshtastic/Helpers/LocalNotificationManager.swift b/Meshtastic/Helpers/LocalNotificationManager.swift index c51a2af3..6cad10a5 100644 --- a/Meshtastic/Helpers/LocalNotificationManager.swift +++ b/Meshtastic/Helpers/LocalNotificationManager.swift @@ -70,8 +70,8 @@ class LocalNotificationManager { if notification.critical { content.sound = UNNotificationSound.defaultCritical } - - let request = UNNotificationRequest(identifier: notification.id, content: content, trigger: nil) + let trigger = UNTimeIntervalNotificationTrigger(timeInterval: 1, repeats: false) + let request = UNNotificationRequest(identifier: notification.id, content: content, trigger: trigger) UNUserNotificationCenter.current().add(request) { error in if let error { diff --git a/Meshtastic/Views/Settings/UserConfig.swift b/Meshtastic/Views/Settings/UserConfig.swift index f2e5d394..db64233e 100644 --- a/Meshtastic/Views/Settings/UserConfig.swift +++ b/Meshtastic/Views/Settings/UserConfig.swift @@ -30,9 +30,9 @@ struct UserConfig: View { @State var overrideDutyCycle = false @State var overrideFrequency: Float = 0.0 @State var txPower = 0 - @FocusState var focusedField: Field? + public var minimumVersion = "2.6.8" let floatFormatter: NumberFormatter = { let formatter = NumberFormatter() formatter.numberStyle = .decimal @@ -97,13 +97,14 @@ struct UserConfig: View { Text("The last 4 of the device MAC address will be appended to the short name to set the device's BLE Name. Short name can be up to 4 bytes long.") .foregroundColor(.gray) .font(.callout) - + let supportedVersion = UserDefaults.firmwareVersion == "0.0.0" || self.minimumVersion.compare(UserDefaults.firmwareVersion, options: .numeric) == .orderedAscending || minimumVersion.compare(UserDefaults.firmwareVersion, options: .numeric) == .orderedSame Toggle(isOn: $isUnmessagable) { Label("Unmessagable", systemImage: "iphone.slash") Text("Used to identify unmonitored or infrastructure nodes so that messaging is not avaliable to nodes that will never respond.") .font(.caption2) } .toggleStyle(SwitchToggleStyle(tint: .accentColor)) + .disabled(!supportedVersion) } // Only manage ham mode for the locally connected node if node?.num ?? 0 > 0 && node?.num ?? 0 == bleManager.connectedPeripheral?.num ?? 0 { From b4d4bde3eed30e4779ee5c0f7c2c8815499e20d4 Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Fri, 23 May 2025 23:41:32 -0700 Subject: [PATCH 063/213] capitalize --- Meshtastic/Views/Nodes/Helpers/Map/MapSettingsForm.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Meshtastic/Views/Nodes/Helpers/Map/MapSettingsForm.swift b/Meshtastic/Views/Nodes/Helpers/Map/MapSettingsForm.swift index 85db444a..6bf4a0c1 100644 --- a/Meshtastic/Views/Nodes/Helpers/Map/MapSettingsForm.swift +++ b/Meshtastic/Views/Nodes/Helpers/Map/MapSettingsForm.swift @@ -29,7 +29,7 @@ struct MapSettingsForm: View { Picker(selection: $mapLayer, label: Text("")) { ForEach(MapLayer.allCases, id: \.self) { layer in if layer != MapLayer.offline { - Text(layer.localized) + Text(layer.localized.capitalized) } } } From 73f1f65553b68b408ae2fd8ee7f8e5f3190a3936 Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Sat, 24 May 2025 00:19:00 -0700 Subject: [PATCH 064/213] Clean up map settings form a bit --- Localizable.xcstrings | 5 +- Meshtastic/Extensions/UserDefaults.swift | 6 +-- .../Map/MapContent/MeshMapContent.swift | 6 +-- .../Nodes/Helpers/Map/MapSettingsForm.swift | 54 ++++++++++--------- 4 files changed, 35 insertions(+), 36 deletions(-) diff --git a/Localizable.xcstrings b/Localizable.xcstrings index 3a04c689..bbde8226 100644 --- a/Localizable.xcstrings +++ b/Localizable.xcstrings @@ -20686,9 +20686,6 @@ } } } - }, - "Only Show Favorites" : { - }, "Open Location Code (aka Plus Codes)" : { "localizations" : { @@ -28467,7 +28464,7 @@ } } }, - "Show Waypoints " : { + "Show Waypoints" : { "localizations" : { "de" : { "stringUnit" : { diff --git a/Meshtastic/Extensions/UserDefaults.swift b/Meshtastic/Extensions/UserDefaults.swift index ee9a9ebc..71313597 100644 --- a/Meshtastic/Extensions/UserDefaults.swift +++ b/Meshtastic/Extensions/UserDefaults.swift @@ -57,7 +57,7 @@ extension UserDefaults { case enableMapTraffic case enableMapPointsOfInterest case enableOfflineMaps - case onlyShowFavoriteNodesMap + case enableMapShowFavorites case mapTileServer case enableOverlayServer case mapOverlayServer @@ -120,8 +120,8 @@ extension UserDefaults { @UserDefault(.enableMapPointsOfInterest, defaultValue: false) static var enableMapPointsOfInterest: Bool - @UserDefault(.onlyShowFavoriteNodesMap, defaultValue: false) - static var onlyShowFavoriteNodesMap: Bool + @UserDefault(.enableMapShowFavorites, defaultValue: false) + static var enableMapShowFavorites: Bool @UserDefault(.enableDetectionNotifications, defaultValue: false) static var enableDetectionNotifications: Bool diff --git a/Meshtastic/Views/Nodes/Helpers/Map/MapContent/MeshMapContent.swift b/Meshtastic/Views/Nodes/Helpers/Map/MapContent/MeshMapContent.swift index ba29cc1a..832ae9c6 100644 --- a/Meshtastic/Views/Nodes/Helpers/Map/MapContent/MeshMapContent.swift +++ b/Meshtastic/Views/Nodes/Helpers/Map/MapContent/MeshMapContent.swift @@ -15,7 +15,7 @@ struct MeshMapContent: MapContent { @AppStorage("meshMapShowNodeHistory") private var showNodeHistory = false @AppStorage("meshMapShowRouteLines") private var showRouteLines = false @AppStorage("enableMapConvexHull") private var showConvexHull = false - @AppStorage("onlyShowFavoriteNodesMap") private var favoriteNodesOnly = false + @AppStorage("enableMapShowFavorites") private var showFavorites = false @Binding var showTraffic: Bool @Binding var showPointsOfInterest: Bool @Binding var selectedMapLayer: MapLayer @@ -40,7 +40,7 @@ struct MeshMapContent: MapContent { @MapContentBuilder var positionAnnotations: some MapContent { ForEach(positions, id: \.id) { position in - if !favoriteNodesOnly || (position.nodePosition?.favorite == true) { + if !showFavorites || (position.nodePosition?.favorite == true) { /// Node color from node.num let nodeColor = UIColor(hex: UInt32(position.nodePosition?.num ?? 0)) let positionName = position.nodePosition?.user?.longName ?? "?" @@ -61,7 +61,7 @@ struct MeshMapContent: MapContent { .onAppear { self.scale = 1 } - .onChange(of: favoriteNodesOnly) { + .onChange(of: showFavorites) { scale = 0.5 // Reset to initial state DispatchQueue.main.asyncAfter(deadline: .now() + 0.01) { diff --git a/Meshtastic/Views/Nodes/Helpers/Map/MapSettingsForm.swift b/Meshtastic/Views/Nodes/Helpers/Map/MapSettingsForm.swift index d93da556..8a2ba6bc 100644 --- a/Meshtastic/Views/Nodes/Helpers/Map/MapSettingsForm.swift +++ b/Meshtastic/Views/Nodes/Helpers/Map/MapSettingsForm.swift @@ -12,10 +12,10 @@ struct MapSettingsForm: View { @Environment(\.dismiss) private var dismiss @State private var currentDetent = PresentationDetent.medium @AppStorage("meshMapShowNodeHistory") private var nodeHistory = false - @AppStorage("meshMapShowRouteLines") private var routeLines = false + @AppStorage("meshMapShowRouteLines") private var enableMapRouteLines = false @AppStorage("enableMapConvexHull") private var convexHull = false - @AppStorage("enableMapWaypoints") private var waypoints = true - @AppStorage("onlyShowFavoriteNodesMap") private var favoriteNodesOnly = false + @AppStorage("enableMapWaypoints") private var enableMapWaypoints = true + @AppStorage("enableMapShowFavorites") private var enableMapShowFavorites = false @Binding var traffic: Bool @Binding var pointsOfInterest: Bool @Binding var mapLayer: MapLayer @@ -54,23 +54,25 @@ struct MapSettingsForm: View { .onChange(of: meshMapDistance) { _, newMeshMapDistance in UserDefaults.meshMapDistance = newMeshMapDistance } - Toggle(isOn: $waypoints) { - Label("Show Waypoints ", systemImage: "signpost.right.and.left") + Toggle(isOn: $enableMapWaypoints) { + Label { + Text("Show Waypoints") + } icon: { + Image(systemName: "signpost.right.and.left") + .symbolRenderingMode(.multicolor) + } } - .toggleStyle(SwitchToggleStyle(tint: .accentColor)) - .onTapGesture { - UserDefaults.enableMapWaypoints = !waypoints + .tint(.accentColor) + } + Toggle(isOn: $enableMapShowFavorites) { + Label { + Text("Favorites") + } icon: { + Image(systemName: "star.fill") + .symbolRenderingMode(.multicolor) } } - - Toggle(isOn: $favoriteNodesOnly) { - Label("Only Show Favorites", systemImage: "star.fill") - } - .toggleStyle(SwitchToggleStyle(tint: .accentColor)) - .onTapGesture { - self.favoriteNodesOnly.toggle() - UserDefaults.onlyShowFavoriteNodesMap = self.favoriteNodesOnly - } + .tint(.accentColor) Toggle(isOn: $nodeHistory) { Label("Node History", systemImage: "building.columns.fill") } @@ -79,15 +81,10 @@ struct MapSettingsForm: View { self.nodeHistory.toggle() UserDefaults.enableMapNodeHistoryPins = self.nodeHistory } - Toggle(isOn: $routeLines) { + Toggle(isOn: $enableMapRouteLines) { Label("Route Lines", systemImage: "road.lanes") } - - .toggleStyle(SwitchToggleStyle(tint: .accentColor)) - .onTapGesture { - self.routeLines.toggle() - UserDefaults.enableMapRouteLines = self.routeLines - } + .tint(.accentColor) Toggle(isOn: $convexHull) { Label("Convex Hull", systemImage: "button.angledbottom.horizontal.right") } @@ -105,9 +102,14 @@ struct MapSettingsForm: View { UserDefaults.enableMapTraffic = self.traffic } Toggle(isOn: $pointsOfInterest) { - Label("Points of Interest", systemImage: "mappin.and.ellipse") + Label { + Text("Points of Interest") + } icon: { + Image(systemName: "mappin.and.ellipse") + .symbolRenderingMode(.multicolor) + } } - .toggleStyle(SwitchToggleStyle(tint: .accentColor)) + .tint(.accentColor) .onTapGesture { self.pointsOfInterest.toggle() UserDefaults.enableMapPointsOfInterest = self.pointsOfInterest From 70340fa18f974db4671158ee63d0227a5bf4bc62 Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Sat, 24 May 2025 00:33:27 -0700 Subject: [PATCH 065/213] Fix some linting and ben writing C# warnings --- .../AppIntents/DisconnectNodeIntent.swift | 1 - Meshtastic/AppIntents/ShortcutsProvider.swift | 1 - Meshtastic/Extensions/UserDefaults.swift | 2 +- Meshtastic/Helpers/BLEManager.swift | 5 ----- Meshtastic/Helpers/LocationsHandler.swift | 1 - Meshtastic/Helpers/MeshPackets.swift | 1 - Meshtastic/MeshtasticApp.swift | 17 ++++++++--------- Meshtastic/Views/Helpers/BatteryCompact.swift | 5 +---- .../Views/Helpers/LoRaSignalStrength.swift | 1 - Meshtastic/Views/Messages/UserMessageList.swift | 3 +-- Meshtastic/Views/Nodes/NodeList.swift | 1 - .../Views/Settings/Config/LoRaConfig.swift | 1 - Meshtastic/Views/Settings/UserConfig.swift | 2 +- 13 files changed, 12 insertions(+), 29 deletions(-) diff --git a/Meshtastic/AppIntents/DisconnectNodeIntent.swift b/Meshtastic/AppIntents/DisconnectNodeIntent.swift index 2e9986d0..4f3b4b33 100644 --- a/Meshtastic/AppIntents/DisconnectNodeIntent.swift +++ b/Meshtastic/AppIntents/DisconnectNodeIntent.swift @@ -13,7 +13,6 @@ struct DisconnectNodeIntent: AppIntent { static var description: IntentDescription = "Disconnect the currently connected node" - func perform() async throws -> some IntentResult { if !BLEManager.shared.isConnected { throw AppIntentErrors.AppIntentError.notConnected diff --git a/Meshtastic/AppIntents/ShortcutsProvider.swift b/Meshtastic/AppIntents/ShortcutsProvider.swift index fcc9ffec..d87c06b1 100644 --- a/Meshtastic/AppIntents/ShortcutsProvider.swift +++ b/Meshtastic/AppIntents/ShortcutsProvider.swift @@ -32,7 +32,6 @@ struct ShortcutsProvider: AppShortcutsProvider { "Send a \(.applicationName) group message"], shortTitle: "Group Message", systemImageName: "message") - AppShortcut(intent: DisconnectNodeIntent(), phrases: ["Disconnect \(.applicationName) node", "Disconnect my \(.applicationName) node", diff --git a/Meshtastic/Extensions/UserDefaults.swift b/Meshtastic/Extensions/UserDefaults.swift index 71313597..11539ab2 100644 --- a/Meshtastic/Extensions/UserDefaults.swift +++ b/Meshtastic/Extensions/UserDefaults.swift @@ -119,7 +119,7 @@ extension UserDefaults { @UserDefault(.enableMapPointsOfInterest, defaultValue: false) static var enableMapPointsOfInterest: Bool - + @UserDefault(.enableMapShowFavorites, defaultValue: false) static var enableMapShowFavorites: Bool diff --git a/Meshtastic/Helpers/BLEManager.swift b/Meshtastic/Helpers/BLEManager.swift index 5fa57408..050958e0 100644 --- a/Meshtastic/Helpers/BLEManager.swift +++ b/Meshtastic/Helpers/BLEManager.swift @@ -181,11 +181,9 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate DispatchQueue.main.async { [weak self] in guard let self = self else { return } guard let connectedPeripheral = self.connectedPeripheral else { return } - if self.mqttProxyConnected { self.mqttManager.mqttClientProxy?.disconnect() } - self.automaticallyReconnect = reconnect self.centralManager?.cancelPeripheralConnection(connectedPeripheral.peripheral) self.FROMRADIO_characteristic = nil @@ -1810,13 +1808,11 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate if let nodeInfoData = try? contact.user.serializedData() { dataNodeMessage.payload = nodeInfoData dataNodeMessage.portnum = PortNum.nodeinfoApp - var nodeMeshPacket = MeshPacket() nodeMeshPacket.id = UInt32.random(in: UInt32(UInt8.max).. Int64 { var adminPacket = AdminMessage() adminPacket.setOwner = config diff --git a/Meshtastic/Helpers/LocationsHandler.swift b/Meshtastic/Helpers/LocationsHandler.swift index f7166f70..15bb390a 100644 --- a/Meshtastic/Helpers/LocationsHandler.swift +++ b/Meshtastic/Helpers/LocationsHandler.swift @@ -142,7 +142,6 @@ import CoreData Logger.services.info("📍 [App] Falling back to last known location (age: \(Int(Date().timeIntervalSince1970 - timestamp)) seconds)") return CLLocationCoordinate2D(latitude: lat, longitude: lon) } - // Fallback 2: Default location Logger.services.warning("📍 [App] No Location and no last known location, something is really wrong. Teleporting user to Apple Park") return DefaultLocation diff --git a/Meshtastic/Helpers/MeshPackets.swift b/Meshtastic/Helpers/MeshPackets.swift index fbf64ab9..fe290aa0 100644 --- a/Meshtastic/Helpers/MeshPackets.swift +++ b/Meshtastic/Helpers/MeshPackets.swift @@ -1058,7 +1058,6 @@ func textMessageAppPacket( } } - func waypointPacket (packet: MeshPacket, context: NSManagedObjectContext) { let logString = String.localizedStringWithFormat("Waypoint Packet received from node: %@".localized, String(packet.from)) diff --git a/Meshtastic/MeshtasticApp.swift b/Meshtastic/MeshtasticApp.swift index 9e763aed..57605580 100644 --- a/Meshtastic/MeshtasticApp.swift +++ b/Meshtastic/MeshtasticApp.swift @@ -61,10 +61,9 @@ struct MeshtasticAppleApp: App { Logger.mesh.debug("URL received \(userActivity, privacy: .public)") self.incomingUrl = userActivity.webpageURL self.saveChannels = false - if (self.incomingUrl?.absoluteString.lowercased().contains("meshtastic.org/v/#") == true) { + if self.incomingUrl?.absoluteString.lowercased().contains("meshtastic.org/v/#") == true { handleContactUrl(url: self.incomingUrl!) - } - else if (self.incomingUrl?.absoluteString.lowercased().contains("meshtastic.org/e/#") == true) { + } 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 if (self.incomingUrl?.absoluteString.lowercased().contains("?")) != nil { @@ -153,19 +152,19 @@ struct MeshtasticAppleApp: App { let components = self.incomingUrl?.absoluteString.components(separatedBy: "#") ?? [] // Extract contact information from the URL if let contactData = components.last { - + let decodedString = contactData.base64urlToBase64() if let decodedData = Data(base64Encoded: decodedString) { do { let contact = try MeshtasticProtobufs.SharedContact(serializedBytes: decodedData) - + // Show an alert to confirm adding the contact let alertController = UIAlertController( title: "Add Contact", message: "Would you like to add \(contact.user.longName) as a contact?", preferredStyle: .alert ) - + alertController.addAction(UIAlertAction( title: "Yes", style: .default, @@ -174,13 +173,13 @@ struct MeshtasticAppleApp: App { Logger.services.debug("Contact added from URL: \(success ? "success" : "failed")") } )) - + alertController.addAction(UIAlertAction( title: "No", style: .cancel, handler: nil )) - + // Present the alert if let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene, let rootViewController = windowScene.windows.first?.rootViewController { @@ -189,7 +188,7 @@ struct MeshtasticAppleApp: App { Logger.services.debug("Contact data extracted from URL: \(contactData, privacy: .public)") } catch { Logger.services.error("Failed to parse contact data: \(error.localizedDescription, privacy: .public)") - + // Show error alert to user if let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene, let rootViewController = windowScene.windows.first?.rootViewController { diff --git a/Meshtastic/Views/Helpers/BatteryCompact.swift b/Meshtastic/Views/Helpers/BatteryCompact.swift index f2142534..f04a5c99 100644 --- a/Meshtastic/Views/Helpers/BatteryCompact.swift +++ b/Meshtastic/Views/Helpers/BatteryCompact.swift @@ -19,7 +19,6 @@ struct BatteryCompact: View { // Check for plugged in state let isPluggedIn = batteryLevel > 100 let isCharging = batteryLevel == 100 - // Battery icon selection based on level if isPluggedIn { Image(systemName: "powerplug") @@ -64,7 +63,6 @@ struct BatteryCompact: View { .symbolRenderingMode(.multicolor) .accessibilityHidden(true) } - // Battery text label if isPluggedIn { Text("PWD") @@ -89,7 +87,6 @@ struct BatteryCompact: View { .foregroundColor(color) .symbolRenderingMode(.multicolor) .accessibilityHidden(true) - Text(verbatim: "?") .foregroundStyle(.secondary) .font(font) @@ -100,7 +97,7 @@ struct BatteryCompact: View { .accessibilityElement(children: .ignore) .accessibilityLabel(NSLocalizedString("Battery Level", comment: "VoiceOver label for battery gauge")) // Set appropriate value based on the battery state using a computed property - .accessibilityValue(batteryLevel.map { level in + .accessibilityValue(batteryLevel.map { level in if level > 100 { // Plugged in - same as PWD visual indicator return "Plugged in".localized diff --git a/Meshtastic/Views/Helpers/LoRaSignalStrength.swift b/Meshtastic/Views/Helpers/LoRaSignalStrength.swift index b58e8987..2a8fc3e7 100644 --- a/Meshtastic/Views/Helpers/LoRaSignalStrength.swift +++ b/Meshtastic/Views/Helpers/LoRaSignalStrength.swift @@ -65,7 +65,6 @@ struct LoRaSignalStrengthMeter_Previews: PreviewProvider { LoRaSignalStrengthMeter(snr: -26.0, rssi: -128, preset: ModemPresets.longFast, compact: true) .padding(.bottom) }.padding() - HStack { // Good LoRaSignalStrengthMeter(snr: -1, rssi: -114, preset: ModemPresets.longFast, compact: false) diff --git a/Meshtastic/Views/Messages/UserMessageList.swift b/Meshtastic/Views/Messages/UserMessageList.swift index ce9b0f33..2a4756fd 100644 --- a/Meshtastic/Views/Messages/UserMessageList.swift +++ b/Meshtastic/Views/Messages/UserMessageList.swift @@ -24,7 +24,7 @@ struct UserMessageList: View { @State private var hasReachedBottom = false @State private var gotFirstUnreadMessage: Bool = false @State private var messageToHighlight: Int64 = 0 - + var body: some View { VStack { ScrollViewReader { scrollView in @@ -51,7 +51,6 @@ struct UserMessageList: View { messageToHighlight = messageNum } scrollView.scrollTo(messageNum, anchor: .center) - // Reset highlight after delay Task { try? await Task.sleep(nanoseconds: 1_000_000_000) // 1 second diff --git a/Meshtastic/Views/Nodes/NodeList.swift b/Meshtastic/Views/Nodes/NodeList.swift index e3a91032..b0d602c2 100644 --- a/Meshtastic/Views/Nodes/NodeList.swift +++ b/Meshtastic/Views/Nodes/NodeList.swift @@ -364,7 +364,6 @@ struct NodeList: View { Logger.services.info("NodeList directly updated from notification for node: \(nodeNum, privacy: .public)") } } - Task { await searchNodeList() } diff --git a/Meshtastic/Views/Settings/Config/LoRaConfig.swift b/Meshtastic/Views/Settings/Config/LoRaConfig.swift index 1aae6ea4..452da960 100644 --- a/Meshtastic/Views/Settings/Config/LoRaConfig.swift +++ b/Meshtastic/Views/Settings/Config/LoRaConfig.swift @@ -248,7 +248,6 @@ struct LoRaConfig: View { /// 2.5 Administration with session passkey let expiration = node.sessionExpiration ?? Date() if expiration < Date() || node.loRaConfig == nil { - Logger.mesh.info("⚙️ Empty or expired lora config requesting via PKI admin") if connectedNode.user != nil && node.user != nil { _ = bleManager.requestLoRaConfig(fromUser: connectedNode.user!, toUser: node.user!, adminIndex: connectedNode.myInfo?.adminIndex ?? 0) diff --git a/Meshtastic/Views/Settings/UserConfig.swift b/Meshtastic/Views/Settings/UserConfig.swift index db64233e..1ffa4ce1 100644 --- a/Meshtastic/Views/Settings/UserConfig.swift +++ b/Meshtastic/Views/Settings/UserConfig.swift @@ -184,7 +184,7 @@ struct UserConfig: View { } else { var ham = HamParameters() ham.shortName = shortName - //ham.isUnmessagable = isUnmessagable + // ham.isUnmessagable = isUnmessagable ham.callSign = longName ham.txPower = Int32(txPower) ham.frequency = overrideFrequency From 825b198e602a2245c361532e285e1f967276ec2f Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Sun, 25 May 2025 08:27:35 -0700 Subject: [PATCH 066/213] Bump minimum firmware version for unmessagable configuration --- Meshtastic/Views/Settings/UserConfig.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Meshtastic/Views/Settings/UserConfig.swift b/Meshtastic/Views/Settings/UserConfig.swift index 1ffa4ce1..708584a4 100644 --- a/Meshtastic/Views/Settings/UserConfig.swift +++ b/Meshtastic/Views/Settings/UserConfig.swift @@ -32,7 +32,7 @@ struct UserConfig: View { @State var txPower = 0 @FocusState var focusedField: Field? - public var minimumVersion = "2.6.8" + public var minimumVersion = "2.6.9" let floatFormatter: NumberFormatter = { let formatter = NumberFormatter() formatter.numberStyle = .decimal From d56f55d6f098a8cabf5870525d1803a885c37452 Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Sun, 25 May 2025 09:00:48 -0700 Subject: [PATCH 067/213] Integrate some copilot review suggestions --- Meshtastic/MeshtasticApp.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Meshtastic/MeshtasticApp.swift b/Meshtastic/MeshtasticApp.swift index 57605580..119ebd50 100644 --- a/Meshtastic/MeshtasticApp.swift +++ b/Meshtastic/MeshtasticApp.swift @@ -149,7 +149,7 @@ struct MeshtasticAppleApp: App { } func handleContactUrl(url: URL) { - let components = self.incomingUrl?.absoluteString.components(separatedBy: "#") ?? [] + let components = url.absoluteString.components(separatedBy: "#") // Extract contact information from the URL if let contactData = components.last { @@ -161,7 +161,7 @@ struct MeshtasticAppleApp: App { // Show an alert to confirm adding the contact let alertController = UIAlertController( title: "Add Contact", - message: "Would you like to add \(contact.user.longName) as a contact?", + message: "Would you like to add \(contact.user.longName) as a contact?", preferredStyle: .alert ) From d84e4371805548fde24dedbdfb02cdc4770d809e Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Sun, 25 May 2025 09:14:27 -0700 Subject: [PATCH 068/213] Add alert for firmware version to contact scanning function --- Meshtastic/MeshtasticApp.swift | 121 ++++++++++++++++++--------------- 1 file changed, 66 insertions(+), 55 deletions(-) diff --git a/Meshtastic/MeshtasticApp.swift b/Meshtastic/MeshtasticApp.swift index 119ebd50..685ae640 100644 --- a/Meshtastic/MeshtasticApp.swift +++ b/Meshtastic/MeshtasticApp.swift @@ -9,14 +9,9 @@ import MeshtasticProtobufs @main struct MeshtasticAppleApp: App { - @UIApplicationDelegateAdaptor(MeshtasticAppDelegate.self) - private var appDelegate + @UIApplicationDelegateAdaptor(MeshtasticAppDelegate.self) private var appDelegate - @ObservedObject - var appState: AppState - -// @ObservedObject -// private var bleManager: BLEManager + @ObservedObject var appState: AppState private let persistenceController: PersistenceController @@ -25,6 +20,7 @@ struct MeshtasticAppleApp: App { @State var incomingUrl: URL? @State var channelSettings: String? @State var addChannels = false + public var minimumContactVersion = "2.6.9" init() { let persistenceController = PersistenceController.shared @@ -149,56 +145,71 @@ struct MeshtasticAppleApp: App { } func handleContactUrl(url: URL) { - let components = url.absoluteString.components(separatedBy: "#") - // Extract contact information from the URL - if let contactData = components.last { - - let decodedString = contactData.base64urlToBase64() - if let decodedData = Data(base64Encoded: decodedString) { - do { - let contact = try MeshtasticProtobufs.SharedContact(serializedBytes: decodedData) - - // Show an alert to confirm adding the contact - let alertController = UIAlertController( - title: "Add Contact", - message: "Would you like to add \(contact.user.longName) as a contact?", - preferredStyle: .alert - ) - - alertController.addAction(UIAlertAction( - title: "Yes", - style: .default, - handler: { _ in - let success = BLEManager.shared.addContactFromURL(base64UrlString: contactData) - Logger.services.debug("Contact added from URL: \(success ? "success" : "failed")") - } - )) - - alertController.addAction(UIAlertAction( - title: "No", - style: .cancel, - handler: nil - )) - - // Present the alert - if let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene, - let rootViewController = windowScene.windows.first?.rootViewController { - rootViewController.present(alertController, animated: true) - } - Logger.services.debug("Contact data extracted from URL: \(contactData, privacy: .public)") - } catch { - Logger.services.error("Failed to parse contact data: \(error.localizedDescription, privacy: .public)") - - // Show error alert to user - if let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene, - let rootViewController = windowScene.windows.first?.rootViewController { - let errorAlert = UIAlertController( - title: "Error", - message: "Could not process contact information. Invalid format.", + let supportedVersion = UserDefaults.firmwareVersion == "0.0.0" || self.minimumContactVersion.compare(UserDefaults.firmwareVersion, options: .numeric) == .orderedAscending || minimumContactVersion.compare(UserDefaults.firmwareVersion, options: .numeric) == .orderedSame + if !supportedVersion { + // Show an alert letting the user know they need to upgrade their firmware to use the contact import. + let alertController = UIAlertController( + title: "Firmware Upgrade Required", + message: "In order to import contacts via a QR code you need firmware version 2.6.9 or greater.", + preferredStyle: .alert + ) + alertController.addAction(UIAlertAction( + title: "Close", + style: .cancel, + handler: nil + )) + // Present the alert + if let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene, + let rootViewController = windowScene.windows.first?.rootViewController { + rootViewController.present(alertController, animated: true) + } + Logger.services.debug("User Alerted that a firmware upgrade is required to import contacts.") + } else { + let components = url.absoluteString.components(separatedBy: "#") + // Extract contact information from the URL + if let contactData = components.last { + let decodedString = contactData.base64urlToBase64() + if let decodedData = Data(base64Encoded: decodedString) { + do { + let contact = try MeshtasticProtobufs.SharedContact(serializedBytes: decodedData) + // Show an alert to confirm adding the contact + let alertController = UIAlertController( + title: "Add Contact", + message: "Would you like to add \(contact.user.longName) as a contact?", preferredStyle: .alert ) - errorAlert.addAction(UIAlertAction(title: "OK", style: .default)) - rootViewController.present(errorAlert, animated: true) + alertController.addAction(UIAlertAction( + title: "Yes", + style: .default, + handler: { _ in + let success = BLEManager.shared.addContactFromURL(base64UrlString: contactData) + Logger.services.debug("Contact added from URL: \(success ? "success" : "failed")") + } + )) + alertController.addAction(UIAlertAction( + title: "No", + style: .cancel, + handler: nil + )) + // Present the alert + if let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene, + let rootViewController = windowScene.windows.first?.rootViewController { + rootViewController.present(alertController, animated: true) + } + Logger.services.debug("Contact data extracted from URL: \(contactData, privacy: .public)") + } catch { + Logger.services.error("Failed to parse contact data: \(error.localizedDescription, privacy: .public)") + // Show error alert to user + if let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene, + let rootViewController = windowScene.windows.first?.rootViewController { + let errorAlert = UIAlertController( + title: "Error", + message: "Could not process contact information. Invalid format.", + preferredStyle: .alert + ) + errorAlert.addAction(UIAlertAction(title: "OK", style: .default)) + rootViewController.present(errorAlert, animated: true) + } } } } From 16ad1ecfce0978624cc3d66251c84858c926869c Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Sun, 25 May 2025 09:25:58 -0700 Subject: [PATCH 069/213] Only share messagable contacts --- Localizable.xcstrings | 5 ++++- .../CoreData/NodeInfoEntityToNodeInfo.swift | 4 +++- Meshtastic/Views/Nodes/Helpers/NodeDetail.swift | 4 ++-- Meshtastic/Views/Nodes/NodeList.swift | 14 ++++++++------ 4 files changed, 17 insertions(+), 10 deletions(-) diff --git a/Localizable.xcstrings b/Localizable.xcstrings index bbde8226..eec7ee0d 100644 --- a/Localizable.xcstrings +++ b/Localizable.xcstrings @@ -17666,6 +17666,9 @@ } } } + }, + "Messaging" : { + }, "Metric" : { "localizations" : { @@ -32903,7 +32906,7 @@ "Unmessagable" : { }, - "Unmonitored or Infrastructure" : { + "Unmonitored" : { }, "Unset" : { diff --git a/Meshtastic/Extensions/CoreData/NodeInfoEntityToNodeInfo.swift b/Meshtastic/Extensions/CoreData/NodeInfoEntityToNodeInfo.swift index 0c487346..034651be 100644 --- a/Meshtastic/Extensions/CoreData/NodeInfoEntityToNodeInfo.swift +++ b/Meshtastic/Extensions/CoreData/NodeInfoEntityToNodeInfo.swift @@ -15,7 +15,9 @@ extension NodeInfoEntity { userProto.shortName = user.shortName ?? "" userProto.hwModel = HardwareModel(rawValue: Int(user.hwModelId)) ?? .unset userProto.isLicensed = user.isLicensed - userProto.isUnmessagable = false + if userProto.hasIsUnmessagable == true { + userProto.isUnmessagable = user.unmessagable + } userProto.role = Config.DeviceConfig.Role(rawValue: Int(user.role)) ?? .client userProto.publicKey = user.publicKey?.subdata(in: 0.. Date: Sun, 25 May 2025 09:28:17 -0700 Subject: [PATCH 070/213] Remove extraneous coredata dependancy --- Meshtastic/Helpers/LocationsHandler.swift | 1 - 1 file changed, 1 deletion(-) diff --git a/Meshtastic/Helpers/LocationsHandler.swift b/Meshtastic/Helpers/LocationsHandler.swift index 15bb390a..23493604 100644 --- a/Meshtastic/Helpers/LocationsHandler.swift +++ b/Meshtastic/Helpers/LocationsHandler.swift @@ -8,7 +8,6 @@ import SwiftUI import CoreLocation import OSLog -import CoreData // Shared state that manages the `CLLocationManager` and `CLBackgroundActivitySession`. @MainActor class LocationsHandler: ObservableObject { From 7b1c6c2078144db58ce039dab391c8c589191add Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Tue, 27 May 2025 09:12:25 -0700 Subject: [PATCH 071/213] Bump version --- Meshtastic.xcodeproj/project.pbxproj | 8 ++++---- Meshtastic/Views/Settings/Firmware.swift | 3 +++ 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/Meshtastic.xcodeproj/project.pbxproj b/Meshtastic.xcodeproj/project.pbxproj index 67778f10..877cd45a 100644 --- a/Meshtastic.xcodeproj/project.pbxproj +++ b/Meshtastic.xcodeproj/project.pbxproj @@ -1806,7 +1806,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 2.6.3; + MARKETING_VERSION = 2.6.4; PRODUCT_BUNDLE_IDENTIFIER = gvh.MeshtasticClient; PRODUCT_NAME = "$(TARGET_NAME)"; SUPPORTS_MACCATALYST = YES; @@ -1839,7 +1839,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 2.6.3; + MARKETING_VERSION = 2.6.4; PRODUCT_BUNDLE_IDENTIFIER = gvh.MeshtasticClient; PRODUCT_NAME = "$(TARGET_NAME)"; SUPPORTS_MACCATALYST = YES; @@ -1870,7 +1870,7 @@ "@executable_path/Frameworks", "@executable_path/../../Frameworks", ); - MARKETING_VERSION = 2.6.3; + MARKETING_VERSION = 2.6.4; PRODUCT_BUNDLE_IDENTIFIER = gvh.MeshtasticClient.Widgets; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; @@ -1902,7 +1902,7 @@ "@executable_path/Frameworks", "@executable_path/../../Frameworks", ); - MARKETING_VERSION = 2.6.3; + MARKETING_VERSION = 2.6.4; PRODUCT_BUNDLE_IDENTIFIER = gvh.MeshtasticClient.Widgets; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; diff --git a/Meshtastic/Views/Settings/Firmware.swift b/Meshtastic/Views/Settings/Firmware.swift index ad885d86..0e21850a 100644 --- a/Meshtastic/Views/Settings/Firmware.swift +++ b/Meshtastic/Views/Settings/Firmware.swift @@ -197,6 +197,9 @@ struct Firmware: View { } Api().loadFirmwareReleaseData { (fw) in latestStable = fw.releases.stable.first + let archString = currentDevice?.architecture.rawValue ?? "" + let ls = fw.releases.stable.first(where: { $0.zipURL.contains(archString) == true }) + latestStable = fw.releases.stable.first(where: { $0.zipURL.contains(archString) == true }) latestAlpha = fw.releases.alpha.first } } From cf9d832ccef170bbe313144a79d304ce9b94ff72 Mon Sep 17 00:00:00 2001 From: Ben Meadors Date: Wed, 28 May 2025 14:21:13 -0500 Subject: [PATCH 072/213] Updated protos and generation script --- .../Sources/meshtastic/admin.pb.swift | 186 +++++++++ .../Sources/meshtastic/config.pb.swift | 10 + .../Sources/meshtastic/deviceonly.pb.swift | 16 + .../Sources/meshtastic/mesh.pb.swift | 389 ++++++++++++++++++ .../Sources/meshtastic/portnums.pb.swift | 8 + .../Sources/meshtastic/telemetry.pb.swift | 217 ++++++++++ protobufs | 2 +- scripts/gen_protos.sh | 4 + 8 files changed, 831 insertions(+), 1 deletion(-) diff --git a/MeshtasticProtobufs/Sources/meshtastic/admin.pb.swift b/MeshtasticProtobufs/Sources/meshtastic/admin.pb.swift index 3f259682..188799b9 100644 --- a/MeshtasticProtobufs/Sources/meshtastic/admin.pb.swift +++ b/MeshtasticProtobufs/Sources/meshtastic/admin.pb.swift @@ -498,6 +498,16 @@ public struct AdminMessage: @unchecked Sendable { set {payloadVariant = .addContact(newValue)} } + /// + /// Initiate or respond to a key verification request + public var keyVerification: KeyVerificationAdmin { + get { + if case .keyVerification(let v)? = payloadVariant {return v} + return KeyVerificationAdmin() + } + set {payloadVariant = .keyVerification(newValue)} + } + /// /// Tell the node to factory reset config everything; all device state and configuration will be returned to factory defaults and BLE bonds will be cleared. public var factoryResetDevice: Int32 { @@ -719,6 +729,9 @@ public struct AdminMessage: @unchecked Sendable { /// Add a contact (User) to the nodedb case addContact(SharedContact) /// + /// Initiate or respond to a key verification request + case keyVerification(KeyVerificationAdmin) + /// /// Tell the node to factory reset config everything; all device state and configuration will be returned to factory defaults and BLE bonds will be cleared. case factoryResetDevice(Int32) /// @@ -1077,6 +1090,98 @@ public struct SharedContact: Sendable { fileprivate var _user: User? = nil } +/// +/// This message is used by a client to initiate or complete a key verification +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. + + public var messageType: KeyVerificationAdmin.MessageType = .initiateVerification + + /// + /// The nodenum we're requesting + public var remoteNodenum: UInt32 = 0 + + /// + /// The nonce is used to track the connection + public var nonce: UInt64 = 0 + + /// + /// The 4 digit code generated by the remote node, and communicated outside the mesh + public var securityNumber: UInt32 { + get {return _securityNumber ?? 0} + set {_securityNumber = newValue} + } + /// Returns true if `securityNumber` has been explicitly set. + public var hasSecurityNumber: Bool {return self._securityNumber != nil} + /// Clears the value of `securityNumber`. Subsequent reads from it will return its default value. + public mutating func clearSecurityNumber() {self._securityNumber = nil} + + public var unknownFields = SwiftProtobuf.UnknownStorage() + + /// + /// Three stages of this request. + public enum MessageType: SwiftProtobuf.Enum, Swift.CaseIterable { + public typealias RawValue = Int + + /// + /// This is the first stage, where a client initiates + case initiateVerification // = 0 + + /// + /// After the nonce has been returned over the mesh, the client prompts for the security number + /// And uses this message to provide it to the node. + case provideSecurityNumber // = 1 + + /// + /// Once the user has compared the verification message, this message notifies the node. + case doVerify // = 2 + + /// + /// This is the cancel path, can be taken at any point + case doNotVerify // = 3 + case UNRECOGNIZED(Int) + + public init() { + self = .initiateVerification + } + + public init?(rawValue: Int) { + switch rawValue { + case 0: self = .initiateVerification + case 1: self = .provideSecurityNumber + case 2: self = .doVerify + case 3: self = .doNotVerify + default: self = .UNRECOGNIZED(rawValue) + } + } + + public var rawValue: Int { + switch self { + case .initiateVerification: return 0 + case .provideSecurityNumber: return 1 + case .doVerify: return 2 + case .doNotVerify: return 3 + case .UNRECOGNIZED(let i): return i + } + } + + // The compiler won't synthesize support with the UNRECOGNIZED case. + public static let allCases: [KeyVerificationAdmin.MessageType] = [ + .initiateVerification, + .provideSecurityNumber, + .doVerify, + .doNotVerify, + ] + + } + + public init() {} + + fileprivate var _securityNumber: UInt32? = nil +} + // MARK: - Code below here is support for the SwiftProtobuf runtime. fileprivate let _protobuf_package = "meshtastic" @@ -1130,6 +1235,7 @@ extension AdminMessage: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementat 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"), @@ -1585,6 +1691,19 @@ extension AdminMessage: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementat self.payloadVariant = .addContact(v) } }() + case 67: try { + var v: KeyVerificationAdmin? + var hadOneofValue = false + if let current = self.payloadVariant { + hadOneofValue = true + if case .keyVerification(let m) = current {v = m} + } + try decoder.decodeSingularMessageField(value: &v) + if let v = v { + if hadOneofValue {try decoder.handleConflictingOneOf()} + self.payloadVariant = .keyVerification(v) + } + }() case 94: try { var v: Int32? try decoder.decodeSingularInt32Field(value: &v) @@ -1833,6 +1952,10 @@ extension AdminMessage: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementat guard case .addContact(let v)? = self.payloadVariant else { preconditionFailure() } try visitor.visitSingularMessageField(value: v, fieldNumber: 66) }() + case .keyVerification?: try { + guard case .keyVerification(let v)? = self.payloadVariant else { preconditionFailure() } + try visitor.visitSingularMessageField(value: v, fieldNumber: 67) + }() case .factoryResetDevice?: try { guard case .factoryResetDevice(let v)? = self.payloadVariant else { preconditionFailure() } try visitor.visitSingularInt32Field(value: v, fieldNumber: 94) @@ -2040,3 +2163,66 @@ extension SharedContact: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementa return true } } + +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 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.messageType) }() + case 2: try { try decoder.decodeSingularUInt32Field(value: &self.remoteNodenum) }() + case 3: try { try decoder.decodeSingularUInt64Field(value: &self.nonce) }() + case 4: try { try decoder.decodeSingularUInt32Field(value: &self._securityNumber) }() + default: break + } + } + } + + public func traverse(visitor: inout V) throws { + // The use of inline closures is to circumvent an issue where the compiler + // allocates stack space for every if/case branch local when no optimizations + // are enabled. https://github.com/apple/swift-protobuf/issues/1034 and + // https://github.com/apple/swift-protobuf/issues/1182 + if self.messageType != .initiateVerification { + try visitor.visitSingularEnumField(value: self.messageType, fieldNumber: 1) + } + if self.remoteNodenum != 0 { + try visitor.visitSingularUInt32Field(value: self.remoteNodenum, fieldNumber: 2) + } + if self.nonce != 0 { + try visitor.visitSingularUInt64Field(value: self.nonce, fieldNumber: 3) + } + try { if let v = self._securityNumber { + try visitor.visitSingularUInt32Field(value: v, fieldNumber: 4) + } }() + try unknownFields.traverse(visitor: &visitor) + } + + public static func ==(lhs: KeyVerificationAdmin, rhs: KeyVerificationAdmin) -> Bool { + if lhs.messageType != rhs.messageType {return false} + if lhs.remoteNodenum != rhs.remoteNodenum {return false} + if lhs.nonce != rhs.nonce {return false} + if lhs._securityNumber != rhs._securityNumber {return false} + if lhs.unknownFields != rhs.unknownFields {return false} + return true + } +} + +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"), + ] +} diff --git a/MeshtasticProtobufs/Sources/meshtastic/config.pb.swift b/MeshtasticProtobufs/Sources/meshtastic/config.pb.swift index 55e6e5f4..12a57c69 100644 --- a/MeshtasticProtobufs/Sources/meshtastic/config.pb.swift +++ b/MeshtasticProtobufs/Sources/meshtastic/config.pb.swift @@ -756,6 +756,10 @@ public struct Config: Sendable { /// Flags for enabling/disabling network protocols public var enabledProtocols: UInt32 = 0 + /// + /// Enable/Disable ipv6 support + public var ipv6Enabled: Bool = false + public var unknownFields = SwiftProtobuf.UnknownStorage() public enum AddressMode: SwiftProtobuf.Enum, Swift.CaseIterable { @@ -2385,6 +2389,7 @@ extension Config.NetworkConfig: SwiftProtobuf.Message, SwiftProtobuf._MessageImp 8: .standard(proto: "ipv4_config"), 9: .standard(proto: "rsyslog_server"), 10: .standard(proto: "enabled_protocols"), + 11: .standard(proto: "ipv6_enabled"), ] public mutating func decodeMessage(decoder: inout D) throws { @@ -2402,6 +2407,7 @@ extension Config.NetworkConfig: SwiftProtobuf.Message, SwiftProtobuf._MessageImp case 8: try { try decoder.decodeSingularMessageField(value: &self._ipv4Config) }() case 9: try { try decoder.decodeSingularStringField(value: &self.rsyslogServer) }() case 10: try { try decoder.decodeSingularUInt32Field(value: &self.enabledProtocols) }() + case 11: try { try decoder.decodeSingularBoolField(value: &self.ipv6Enabled) }() default: break } } @@ -2439,6 +2445,9 @@ extension Config.NetworkConfig: SwiftProtobuf.Message, SwiftProtobuf._MessageImp if self.enabledProtocols != 0 { try visitor.visitSingularUInt32Field(value: self.enabledProtocols, fieldNumber: 10) } + if self.ipv6Enabled != false { + try visitor.visitSingularBoolField(value: self.ipv6Enabled, fieldNumber: 11) + } try unknownFields.traverse(visitor: &visitor) } @@ -2452,6 +2461,7 @@ extension Config.NetworkConfig: SwiftProtobuf.Message, SwiftProtobuf._MessageImp if lhs._ipv4Config != rhs._ipv4Config {return false} if lhs.rsyslogServer != rhs.rsyslogServer {return false} if lhs.enabledProtocols != rhs.enabledProtocols {return false} + if lhs.ipv6Enabled != rhs.ipv6Enabled {return false} if lhs.unknownFields != rhs.unknownFields {return false} return true } diff --git a/MeshtasticProtobufs/Sources/meshtastic/deviceonly.pb.swift b/MeshtasticProtobufs/Sources/meshtastic/deviceonly.pb.swift index cbcbda13..acbc9682 100644 --- a/MeshtasticProtobufs/Sources/meshtastic/deviceonly.pb.swift +++ b/MeshtasticProtobufs/Sources/meshtastic/deviceonly.pb.swift @@ -227,6 +227,14 @@ public struct NodeInfoLite: @unchecked Sendable { set {_uniqueStorage()._nextHop = newValue} } + /// + /// Bitfield for storing booleans. + /// LSB 0 is_key_manually_verified + public var bitfield: UInt32 { + get {return _storage._bitfield} + set {_uniqueStorage()._bitfield = newValue} + } + public var unknownFields = SwiftProtobuf.UnknownStorage() public init() {} @@ -608,6 +616,7 @@ extension NodeInfoLite: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementat 10: .standard(proto: "is_favorite"), 11: .standard(proto: "is_ignored"), 12: .standard(proto: "next_hop"), + 13: .same(proto: "bitfield"), ] fileprivate class _StorageClass { @@ -623,6 +632,7 @@ extension NodeInfoLite: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementat var _isFavorite: Bool = false var _isIgnored: Bool = false 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. @@ -649,6 +659,7 @@ extension NodeInfoLite: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementat _isFavorite = source._isFavorite _isIgnored = source._isIgnored _nextHop = source._nextHop + _bitfield = source._bitfield } } @@ -679,6 +690,7 @@ extension NodeInfoLite: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementat case 10: try { try decoder.decodeSingularBoolField(value: &_storage._isFavorite) }() case 11: try { try decoder.decodeSingularBoolField(value: &_storage._isIgnored) }() case 12: try { try decoder.decodeSingularUInt32Field(value: &_storage._nextHop) }() + case 13: try { try decoder.decodeSingularUInt32Field(value: &_storage._bitfield) }() default: break } } @@ -727,6 +739,9 @@ extension NodeInfoLite: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementat if _storage._nextHop != 0 { try visitor.visitSingularUInt32Field(value: _storage._nextHop, fieldNumber: 12) } + if _storage._bitfield != 0 { + try visitor.visitSingularUInt32Field(value: _storage._bitfield, fieldNumber: 13) + } } try unknownFields.traverse(visitor: &visitor) } @@ -748,6 +763,7 @@ extension NodeInfoLite: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementat if _storage._isFavorite != rhs_storage._isFavorite {return false} if _storage._isIgnored != rhs_storage._isIgnored {return false} if _storage._nextHop != rhs_storage._nextHop {return false} + if _storage._bitfield != rhs_storage._bitfield {return false} return true } if !storagesAreEqual {return false} diff --git a/MeshtasticProtobufs/Sources/meshtastic/mesh.pb.swift b/MeshtasticProtobufs/Sources/meshtastic/mesh.pb.swift index d59ec2ed..407d395f 100644 --- a/MeshtasticProtobufs/Sources/meshtastic/mesh.pb.swift +++ b/MeshtasticProtobufs/Sources/meshtastic/mesh.pb.swift @@ -442,6 +442,22 @@ public enum HardwareModel: SwiftProtobuf.Enum, Swift.CaseIterable { /// Elecrow CrowPanel Advance models, ESP32-S3 and TFT with SX1262 radio plugin case crowpanel // = 97 + ///* + /// Lilygo LINK32 board with sensors + case link32 // = 98 + + ///* + /// Seeed Tracker L1 + case seeedWioTrackerL1 // = 99 + + ///* + /// Seeed Tracker L1 EINK driver + case seeedWioTrackerL1Eink // = 100 + + /// + /// Reserved ID for future and past use + case qwantzTinyArms // = 101 + /// /// ------------------------------------------------------------------------------------------------------------------------------------------ /// 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. @@ -553,6 +569,10 @@ public enum HardwareModel: SwiftProtobuf.Enum, Swift.CaseIterable { case 95: self = .seeedSolarNode case 96: self = .nomadstarMeteorPro case 97: self = .crowpanel + case 98: self = .link32 + case 99: self = .seeedWioTrackerL1 + case 100: self = .seeedWioTrackerL1Eink + case 101: self = .qwantzTinyArms case 255: self = .privateHw default: self = .UNRECOGNIZED(rawValue) } @@ -658,6 +678,10 @@ public enum HardwareModel: SwiftProtobuf.Enum, Swift.CaseIterable { case .seeedSolarNode: return 95 case .nomadstarMeteorPro: return 96 case .crowpanel: return 97 + case .link32: return 98 + case .seeedWioTrackerL1: return 99 + case .seeedWioTrackerL1Eink: return 100 + case .qwantzTinyArms: return 101 case .privateHw: return 255 case .UNRECOGNIZED(let i): return i } @@ -763,6 +787,10 @@ public enum HardwareModel: SwiftProtobuf.Enum, Swift.CaseIterable { .seeedSolarNode, .nomadstarMeteorPro, .crowpanel, + .link32, + .seeedWioTrackerL1, + .seeedWioTrackerL1Eink, + .qwantzTinyArms, .privateHw, ] @@ -1820,6 +1848,31 @@ public struct DataMessage: @unchecked Sendable { fileprivate var _bitfield: UInt32? = nil } +/// +/// The actual over-the-mesh message doing KeyVerification +public struct KeyVerification: @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. + + /// + /// random value Selected by the requesting node + public var nonce: UInt64 = 0 + + /// + /// The final authoritative hash, only to be sent by NodeA at the end of the handshake + public var hash1: Data = Data() + + /// + /// The intermediary hash (actually derived from hash1), + /// sent from NodeB to NodeA in response to the initial message. + public var hash2: Data = Data() + + public var unknownFields = SwiftProtobuf.UnknownStorage() + + public init() {} +} + /// /// Waypoint message, used to share arbitrary locations across the mesh public struct Waypoint: Sendable { @@ -2441,6 +2494,15 @@ public struct NodeInfo: @unchecked Sendable { set {_uniqueStorage()._isIgnored = newValue} } + /// + /// True if node public key has been verified. + /// Persists between NodeDB internal clean ups + /// LSB 0 of the bitfield + public var isKeyManuallyVerified: Bool { + get {return _storage._isKeyManuallyVerified} + set {_uniqueStorage()._isKeyManuallyVerified = newValue} + } + public var unknownFields = SwiftProtobuf.UnknownStorage() public init() {} @@ -2903,13 +2965,94 @@ public struct ClientNotification: Sendable { /// The message body of the notification public var message: String = String() + public var payloadVariant: ClientNotification.OneOf_PayloadVariant? = nil + + public var keyVerificationNumberInform: KeyVerificationNumberInform { + get { + if case .keyVerificationNumberInform(let v)? = payloadVariant {return v} + return KeyVerificationNumberInform() + } + set {payloadVariant = .keyVerificationNumberInform(newValue)} + } + + public var keyVerificationNumberRequest: KeyVerificationNumberRequest { + get { + if case .keyVerificationNumberRequest(let v)? = payloadVariant {return v} + return KeyVerificationNumberRequest() + } + set {payloadVariant = .keyVerificationNumberRequest(newValue)} + } + + public var keyVerificationFinal: KeyVerificationFinal { + get { + if case .keyVerificationFinal(let v)? = payloadVariant {return v} + return KeyVerificationFinal() + } + set {payloadVariant = .keyVerificationFinal(newValue)} + } + public var unknownFields = SwiftProtobuf.UnknownStorage() + public enum OneOf_PayloadVariant: Equatable, Sendable { + case keyVerificationNumberInform(KeyVerificationNumberInform) + case keyVerificationNumberRequest(KeyVerificationNumberRequest) + case keyVerificationFinal(KeyVerificationFinal) + + } + public init() {} fileprivate var _replyID: UInt32? = nil } +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. + + public var nonce: UInt64 = 0 + + public var remoteLongname: String = String() + + public var securityNumber: UInt32 = 0 + + public var unknownFields = SwiftProtobuf.UnknownStorage() + + public init() {} +} + +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. + + public var nonce: UInt64 = 0 + + public var remoteLongname: String = String() + + public var unknownFields = SwiftProtobuf.UnknownStorage() + + public init() {} +} + +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. + + public var nonce: UInt64 = 0 + + public var remoteLongname: String = String() + + public var isSender: Bool = false + + public var verificationCharacters: String = String() + + public var unknownFields = SwiftProtobuf.UnknownStorage() + + public init() {} +} + /// /// Individual File info for the device public struct FileInfo: Sendable { @@ -3431,6 +3574,10 @@ extension HardwareModel: SwiftProtobuf._ProtoNameProviding { 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: "QWANTZ_TINY_ARMS"), 255: .same(proto: "PRIVATE_HW"), ] } @@ -4075,6 +4222,50 @@ 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 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.decodeSingularUInt64Field(value: &self.nonce) }() + case 2: try { try decoder.decodeSingularBytesField(value: &self.hash1) }() + case 3: try { try decoder.decodeSingularBytesField(value: &self.hash2) }() + default: break + } + } + } + + public func traverse(visitor: inout V) throws { + if self.nonce != 0 { + try visitor.visitSingularUInt64Field(value: self.nonce, fieldNumber: 1) + } + if !self.hash1.isEmpty { + try visitor.visitSingularBytesField(value: self.hash1, fieldNumber: 2) + } + if !self.hash2.isEmpty { + try visitor.visitSingularBytesField(value: self.hash2, fieldNumber: 3) + } + try unknownFields.traverse(visitor: &visitor) + } + + public static func ==(lhs: KeyVerification, rhs: KeyVerification) -> Bool { + if lhs.nonce != rhs.nonce {return false} + if lhs.hash1 != rhs.hash1 {return false} + if lhs.hash2 != rhs.hash2 {return false} + if lhs.unknownFields != rhs.unknownFields {return false} + return true + } +} + extension Waypoint: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { public static let protoMessageName: String = _protobuf_package + ".Waypoint" public static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ @@ -4511,6 +4702,7 @@ extension NodeInfo: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationB 9: .standard(proto: "hops_away"), 10: .standard(proto: "is_favorite"), 11: .standard(proto: "is_ignored"), + 12: .standard(proto: "is_key_manually_verified"), ] fileprivate class _StorageClass { @@ -4525,6 +4717,7 @@ extension NodeInfo: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationB var _hopsAway: UInt32? = nil var _isFavorite: Bool = false 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. @@ -4550,6 +4743,7 @@ extension NodeInfo: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationB _hopsAway = source._hopsAway _isFavorite = source._isFavorite _isIgnored = source._isIgnored + _isKeyManuallyVerified = source._isKeyManuallyVerified } } @@ -4579,6 +4773,7 @@ extension NodeInfo: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationB case 9: try { try decoder.decodeSingularUInt32Field(value: &_storage._hopsAway) }() case 10: try { try decoder.decodeSingularBoolField(value: &_storage._isFavorite) }() case 11: try { try decoder.decodeSingularBoolField(value: &_storage._isIgnored) }() + case 12: try { try decoder.decodeSingularBoolField(value: &_storage._isKeyManuallyVerified) }() default: break } } @@ -4624,6 +4819,9 @@ extension NodeInfo: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationB if _storage._isIgnored != false { try visitor.visitSingularBoolField(value: _storage._isIgnored, fieldNumber: 11) } + if _storage._isKeyManuallyVerified != false { + try visitor.visitSingularBoolField(value: _storage._isKeyManuallyVerified, fieldNumber: 12) + } } try unknownFields.traverse(visitor: &visitor) } @@ -4644,6 +4842,7 @@ extension NodeInfo: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationB if _storage._hopsAway != rhs_storage._hopsAway {return false} if _storage._isFavorite != rhs_storage._isFavorite {return false} if _storage._isIgnored != rhs_storage._isIgnored {return false} + if _storage._isKeyManuallyVerified != rhs_storage._isKeyManuallyVerified {return false} return true } if !storagesAreEqual {return false} @@ -5146,6 +5345,9 @@ extension ClientNotification: SwiftProtobuf.Message, SwiftProtobuf._MessageImple 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"), ] public mutating func decodeMessage(decoder: inout D) throws { @@ -5158,6 +5360,45 @@ extension ClientNotification: SwiftProtobuf.Message, SwiftProtobuf._MessageImple case 2: try { try decoder.decodeSingularFixed32Field(value: &self.time) }() case 3: try { try decoder.decodeSingularEnumField(value: &self.level) }() case 4: try { try decoder.decodeSingularStringField(value: &self.message) }() + case 11: try { + var v: KeyVerificationNumberInform? + var hadOneofValue = false + if let current = self.payloadVariant { + hadOneofValue = true + if case .keyVerificationNumberInform(let m) = current {v = m} + } + try decoder.decodeSingularMessageField(value: &v) + if let v = v { + if hadOneofValue {try decoder.handleConflictingOneOf()} + self.payloadVariant = .keyVerificationNumberInform(v) + } + }() + case 12: try { + var v: KeyVerificationNumberRequest? + var hadOneofValue = false + if let current = self.payloadVariant { + hadOneofValue = true + if case .keyVerificationNumberRequest(let m) = current {v = m} + } + try decoder.decodeSingularMessageField(value: &v) + if let v = v { + if hadOneofValue {try decoder.handleConflictingOneOf()} + self.payloadVariant = .keyVerificationNumberRequest(v) + } + }() + case 13: try { + var v: KeyVerificationFinal? + var hadOneofValue = false + if let current = self.payloadVariant { + hadOneofValue = true + if case .keyVerificationFinal(let m) = current {v = m} + } + try decoder.decodeSingularMessageField(value: &v) + if let v = v { + if hadOneofValue {try decoder.handleConflictingOneOf()} + self.payloadVariant = .keyVerificationFinal(v) + } + }() default: break } } @@ -5180,6 +5421,21 @@ extension ClientNotification: SwiftProtobuf.Message, SwiftProtobuf._MessageImple if !self.message.isEmpty { try visitor.visitSingularStringField(value: self.message, fieldNumber: 4) } + switch self.payloadVariant { + case .keyVerificationNumberInform?: try { + guard case .keyVerificationNumberInform(let v)? = self.payloadVariant else { preconditionFailure() } + try visitor.visitSingularMessageField(value: v, fieldNumber: 11) + }() + case .keyVerificationNumberRequest?: try { + guard case .keyVerificationNumberRequest(let v)? = self.payloadVariant else { preconditionFailure() } + try visitor.visitSingularMessageField(value: v, fieldNumber: 12) + }() + case .keyVerificationFinal?: try { + guard case .keyVerificationFinal(let v)? = self.payloadVariant else { preconditionFailure() } + try visitor.visitSingularMessageField(value: v, fieldNumber: 13) + }() + case nil: break + } try unknownFields.traverse(visitor: &visitor) } @@ -5188,6 +5444,139 @@ extension ClientNotification: SwiftProtobuf.Message, SwiftProtobuf._MessageImple if lhs.time != rhs.time {return false} if lhs.level != rhs.level {return false} if lhs.message != rhs.message {return false} + if lhs.payloadVariant != rhs.payloadVariant {return false} + if lhs.unknownFields != rhs.unknownFields {return false} + return true + } +} + +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 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.decodeSingularUInt64Field(value: &self.nonce) }() + case 2: try { try decoder.decodeSingularStringField(value: &self.remoteLongname) }() + case 3: try { try decoder.decodeSingularUInt32Field(value: &self.securityNumber) }() + default: break + } + } + } + + public func traverse(visitor: inout V) throws { + if self.nonce != 0 { + try visitor.visitSingularUInt64Field(value: self.nonce, fieldNumber: 1) + } + if !self.remoteLongname.isEmpty { + try visitor.visitSingularStringField(value: self.remoteLongname, fieldNumber: 2) + } + if self.securityNumber != 0 { + try visitor.visitSingularUInt32Field(value: self.securityNumber, fieldNumber: 3) + } + try unknownFields.traverse(visitor: &visitor) + } + + public static func ==(lhs: KeyVerificationNumberInform, rhs: KeyVerificationNumberInform) -> Bool { + if lhs.nonce != rhs.nonce {return false} + if lhs.remoteLongname != rhs.remoteLongname {return false} + if lhs.securityNumber != rhs.securityNumber {return false} + if lhs.unknownFields != rhs.unknownFields {return false} + return true + } +} + +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 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.decodeSingularUInt64Field(value: &self.nonce) }() + case 2: try { try decoder.decodeSingularStringField(value: &self.remoteLongname) }() + default: break + } + } + } + + public func traverse(visitor: inout V) throws { + if self.nonce != 0 { + try visitor.visitSingularUInt64Field(value: self.nonce, fieldNumber: 1) + } + if !self.remoteLongname.isEmpty { + try visitor.visitSingularStringField(value: self.remoteLongname, fieldNumber: 2) + } + try unknownFields.traverse(visitor: &visitor) + } + + public static func ==(lhs: KeyVerificationNumberRequest, rhs: KeyVerificationNumberRequest) -> Bool { + if lhs.nonce != rhs.nonce {return false} + if lhs.remoteLongname != rhs.remoteLongname {return false} + if lhs.unknownFields != rhs.unknownFields {return false} + return true + } +} + +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 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.decodeSingularUInt64Field(value: &self.nonce) }() + case 2: try { try decoder.decodeSingularStringField(value: &self.remoteLongname) }() + case 3: try { try decoder.decodeSingularBoolField(value: &self.isSender) }() + case 4: try { try decoder.decodeSingularStringField(value: &self.verificationCharacters) }() + default: break + } + } + } + + public func traverse(visitor: inout V) throws { + if self.nonce != 0 { + try visitor.visitSingularUInt64Field(value: self.nonce, fieldNumber: 1) + } + if !self.remoteLongname.isEmpty { + try visitor.visitSingularStringField(value: self.remoteLongname, fieldNumber: 2) + } + if self.isSender != false { + try visitor.visitSingularBoolField(value: self.isSender, fieldNumber: 3) + } + if !self.verificationCharacters.isEmpty { + try visitor.visitSingularStringField(value: self.verificationCharacters, fieldNumber: 4) + } + try unknownFields.traverse(visitor: &visitor) + } + + public static func ==(lhs: KeyVerificationFinal, rhs: KeyVerificationFinal) -> Bool { + if lhs.nonce != rhs.nonce {return false} + if lhs.remoteLongname != rhs.remoteLongname {return false} + if lhs.isSender != rhs.isSender {return false} + if lhs.verificationCharacters != rhs.verificationCharacters {return false} if lhs.unknownFields != rhs.unknownFields {return false} return true } diff --git a/MeshtasticProtobufs/Sources/meshtastic/portnums.pb.swift b/MeshtasticProtobufs/Sources/meshtastic/portnums.pb.swift index cac96bc4..182e233c 100644 --- a/MeshtasticProtobufs/Sources/meshtastic/portnums.pb.swift +++ b/MeshtasticProtobufs/Sources/meshtastic/portnums.pb.swift @@ -111,6 +111,10 @@ public enum PortNum: SwiftProtobuf.Enum, Swift.CaseIterable { /// Same as Text Message but used for critical alerts. case alertApp // = 11 + /// + /// Module/port for handling key verification requests. + case keyVerificationApp // = 12 + /// /// Provides a 'ping' service that replies to any packet it receives. /// Also serves as a small example module. @@ -232,6 +236,7 @@ public enum PortNum: SwiftProtobuf.Enum, Swift.CaseIterable { case 9: self = .audioApp case 10: self = .detectionSensorApp case 11: self = .alertApp + case 12: self = .keyVerificationApp case 32: self = .replyApp case 33: self = .ipTunnelApp case 34: self = .paxcounterApp @@ -268,6 +273,7 @@ public enum PortNum: SwiftProtobuf.Enum, Swift.CaseIterable { case .audioApp: return 9 case .detectionSensorApp: return 10 case .alertApp: return 11 + case .keyVerificationApp: return 12 case .replyApp: return 32 case .ipTunnelApp: return 33 case .paxcounterApp: return 34 @@ -304,6 +310,7 @@ public enum PortNum: SwiftProtobuf.Enum, Swift.CaseIterable { .audioApp, .detectionSensorApp, .alertApp, + .keyVerificationApp, .replyApp, .ipTunnelApp, .paxcounterApp, @@ -342,6 +349,7 @@ extension PortNum: SwiftProtobuf._ProtoNameProviding { 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"), diff --git a/MeshtasticProtobufs/Sources/meshtastic/telemetry.pb.swift b/MeshtasticProtobufs/Sources/meshtastic/telemetry.pb.swift index ccf4cfb4..2b89d4bd 100644 --- a/MeshtasticProtobufs/Sources/meshtastic/telemetry.pb.swift +++ b/MeshtasticProtobufs/Sources/meshtastic/telemetry.pb.swift @@ -180,6 +180,10 @@ public enum TelemetrySensorType: SwiftProtobuf.Enum, Swift.CaseIterable { /// /// MAX17261 lipo battery gauge case max17261 // = 38 + + /// + /// PCT2075 Temperature Sensor + case pct2075 // = 39 case UNRECOGNIZED(Int) public init() { @@ -227,6 +231,7 @@ public enum TelemetrySensorType: SwiftProtobuf.Enum, Swift.CaseIterable { case 36: self = .dps310 case 37: self = .rak12035 case 38: self = .max17261 + case 39: self = .pct2075 default: self = .UNRECOGNIZED(rawValue) } } @@ -272,6 +277,7 @@ public enum TelemetrySensorType: SwiftProtobuf.Enum, Swift.CaseIterable { case .dps310: return 36 case .rak12035: return 37 case .max17261: return 38 + case .pct2075: return 39 case .UNRECOGNIZED(let i): return i } } @@ -317,6 +323,7 @@ public enum TelemetrySensorType: SwiftProtobuf.Enum, Swift.CaseIterable { .dps310, .rak12035, .max17261, + .pct2075, ] } @@ -959,6 +966,14 @@ public struct LocalStats: Sendable { /// This will always be zero for ROUTERs/REPEATERs. If this number is high, some other node(s) is/are relaying faster than you. public var numTxRelayCanceled: UInt32 = 0 + /// + /// Number of bytes used in the heap + public var heapTotalBytes: UInt32 = 0 + + /// + /// Number of bytes free in the heap + public var heapFreeBytes: UInt32 = 0 + public var unknownFields = SwiftProtobuf.UnknownStorage() public init() {} @@ -1013,6 +1028,80 @@ public struct HealthMetrics: Sendable { fileprivate var _temperature: Float? = nil } +/// +/// Linux host metrics +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. + + /// + /// Host system uptime + public var uptimeSeconds: UInt32 = 0 + + /// + /// Host system free memory + public var freememBytes: UInt64 = 0 + + /// + /// Host system disk space free for / + public var diskfree1Bytes: UInt64 = 0 + + /// + /// Secondary system disk space free + public var diskfree2Bytes: UInt64 { + get {return _diskfree2Bytes ?? 0} + set {_diskfree2Bytes = newValue} + } + /// Returns true if `diskfree2Bytes` has been explicitly set. + public var hasDiskfree2Bytes: Bool {return self._diskfree2Bytes != nil} + /// Clears the value of `diskfree2Bytes`. Subsequent reads from it will return its default value. + public mutating func clearDiskfree2Bytes() {self._diskfree2Bytes = nil} + + /// + /// Tertiary disk space free + public var diskfree3Bytes: UInt64 { + get {return _diskfree3Bytes ?? 0} + set {_diskfree3Bytes = newValue} + } + /// Returns true if `diskfree3Bytes` has been explicitly set. + public var hasDiskfree3Bytes: Bool {return self._diskfree3Bytes != nil} + /// Clears the value of `diskfree3Bytes`. Subsequent reads from it will return its default value. + public mutating func clearDiskfree3Bytes() {self._diskfree3Bytes = nil} + + /// + /// Host system one minute load in 1/100ths + public var load1: UInt32 = 0 + + /// + /// Host system five minute load in 1/100ths + public var load5: UInt32 = 0 + + /// + /// Host system fifteen minute load in 1/100ths + public var load15: UInt32 = 0 + + /// + /// Optional User-provided string for arbitrary host system information + /// that doesn't make sense as a dedicated entry. + public var userString: String { + get {return _userString ?? String()} + set {_userString = newValue} + } + /// Returns true if `userString` has been explicitly set. + public var hasUserString: Bool {return self._userString != nil} + /// Clears the value of `userString`. Subsequent reads from it will return its default value. + public mutating func clearUserString() {self._userString = nil} + + public var unknownFields = SwiftProtobuf.UnknownStorage() + + public init() {} + + fileprivate var _diskfree2Bytes: UInt64? = nil + fileprivate var _diskfree3Bytes: UInt64? = nil + fileprivate var _userString: String? = nil +} + /// /// Types of Measurements the telemetry module is equipped to handle public struct Telemetry: Sendable { @@ -1086,6 +1175,16 @@ public struct Telemetry: Sendable { set {variant = .healthMetrics(newValue)} } + /// + /// Linux host metrics + public var hostMetrics: HostMetrics { + get { + if case .hostMetrics(let v)? = variant {return v} + return HostMetrics() + } + set {variant = .hostMetrics(newValue)} + } + public var unknownFields = SwiftProtobuf.UnknownStorage() public enum OneOf_Variant: Equatable, Sendable { @@ -1107,6 +1206,9 @@ public struct Telemetry: Sendable { /// /// Health telemetry metrics case healthMetrics(HealthMetrics) + /// + /// Linux host metrics + case hostMetrics(HostMetrics) } @@ -1178,6 +1280,7 @@ extension TelemetrySensorType: SwiftProtobuf._ProtoNameProviding { 36: .same(proto: "DPS310"), 37: .same(proto: "RAK12035"), 38: .same(proto: "MAX17261"), + 39: .same(proto: "PCT2075"), ] } @@ -1673,6 +1776,8 @@ extension LocalStats: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementatio 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 mutating func decodeMessage(decoder: inout D) throws { @@ -1692,6 +1797,8 @@ extension LocalStats: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementatio case 9: try { try decoder.decodeSingularUInt32Field(value: &self.numRxDupe) }() case 10: try { try decoder.decodeSingularUInt32Field(value: &self.numTxRelay) }() 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) }() default: break } } @@ -1731,6 +1838,12 @@ extension LocalStats: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementatio if self.numTxRelayCanceled != 0 { try visitor.visitSingularUInt32Field(value: self.numTxRelayCanceled, fieldNumber: 11) } + if self.heapTotalBytes != 0 { + try visitor.visitSingularUInt32Field(value: self.heapTotalBytes, fieldNumber: 12) + } + if self.heapFreeBytes != 0 { + try visitor.visitSingularUInt32Field(value: self.heapFreeBytes, fieldNumber: 13) + } try unknownFields.traverse(visitor: &visitor) } @@ -1746,6 +1859,8 @@ extension LocalStats: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementatio if lhs.numRxDupe != rhs.numRxDupe {return false} if lhs.numTxRelay != rhs.numTxRelay {return false} if lhs.numTxRelayCanceled != rhs.numTxRelayCanceled {return false} + if lhs.heapTotalBytes != rhs.heapTotalBytes {return false} + if lhs.heapFreeBytes != rhs.heapFreeBytes {return false} if lhs.unknownFields != rhs.unknownFields {return false} return true } @@ -1799,6 +1914,90 @@ 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 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.decodeSingularUInt32Field(value: &self.uptimeSeconds) }() + case 2: try { try decoder.decodeSingularUInt64Field(value: &self.freememBytes) }() + case 3: try { try decoder.decodeSingularUInt64Field(value: &self.diskfree1Bytes) }() + case 4: try { try decoder.decodeSingularUInt64Field(value: &self._diskfree2Bytes) }() + case 5: try { try decoder.decodeSingularUInt64Field(value: &self._diskfree3Bytes) }() + case 6: try { try decoder.decodeSingularUInt32Field(value: &self.load1) }() + case 7: try { try decoder.decodeSingularUInt32Field(value: &self.load5) }() + case 8: try { try decoder.decodeSingularUInt32Field(value: &self.load15) }() + case 9: try { try decoder.decodeSingularStringField(value: &self._userString) }() + default: break + } + } + } + + public func traverse(visitor: inout V) throws { + // The use of inline closures is to circumvent an issue where the compiler + // allocates stack space for every if/case branch local when no optimizations + // are enabled. https://github.com/apple/swift-protobuf/issues/1034 and + // https://github.com/apple/swift-protobuf/issues/1182 + if self.uptimeSeconds != 0 { + try visitor.visitSingularUInt32Field(value: self.uptimeSeconds, fieldNumber: 1) + } + if self.freememBytes != 0 { + try visitor.visitSingularUInt64Field(value: self.freememBytes, fieldNumber: 2) + } + if self.diskfree1Bytes != 0 { + try visitor.visitSingularUInt64Field(value: self.diskfree1Bytes, fieldNumber: 3) + } + try { if let v = self._diskfree2Bytes { + try visitor.visitSingularUInt64Field(value: v, fieldNumber: 4) + } }() + try { if let v = self._diskfree3Bytes { + try visitor.visitSingularUInt64Field(value: v, fieldNumber: 5) + } }() + if self.load1 != 0 { + try visitor.visitSingularUInt32Field(value: self.load1, fieldNumber: 6) + } + if self.load5 != 0 { + try visitor.visitSingularUInt32Field(value: self.load5, fieldNumber: 7) + } + if self.load15 != 0 { + try visitor.visitSingularUInt32Field(value: self.load15, fieldNumber: 8) + } + try { if let v = self._userString { + try visitor.visitSingularStringField(value: v, fieldNumber: 9) + } }() + try unknownFields.traverse(visitor: &visitor) + } + + public static func ==(lhs: HostMetrics, rhs: HostMetrics) -> Bool { + if lhs.uptimeSeconds != rhs.uptimeSeconds {return false} + if lhs.freememBytes != rhs.freememBytes {return false} + if lhs.diskfree1Bytes != rhs.diskfree1Bytes {return false} + if lhs._diskfree2Bytes != rhs._diskfree2Bytes {return false} + if lhs._diskfree3Bytes != rhs._diskfree3Bytes {return false} + if lhs.load1 != rhs.load1 {return false} + if lhs.load5 != rhs.load5 {return false} + if lhs.load15 != rhs.load15 {return false} + if lhs._userString != rhs._userString {return false} + if lhs.unknownFields != rhs.unknownFields {return false} + return true + } +} + extension Telemetry: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { public static let protoMessageName: String = _protobuf_package + ".Telemetry" public static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ @@ -1809,6 +2008,7 @@ extension Telemetry: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementation 5: .standard(proto: "power_metrics"), 6: .standard(proto: "local_stats"), 7: .standard(proto: "health_metrics"), + 8: .standard(proto: "host_metrics"), ] public mutating func decodeMessage(decoder: inout D) throws { @@ -1896,6 +2096,19 @@ extension Telemetry: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementation self.variant = .healthMetrics(v) } }() + case 8: try { + var v: HostMetrics? + var hadOneofValue = false + if let current = self.variant { + hadOneofValue = true + if case .hostMetrics(let m) = current {v = m} + } + try decoder.decodeSingularMessageField(value: &v) + if let v = v { + if hadOneofValue {try decoder.handleConflictingOneOf()} + self.variant = .hostMetrics(v) + } + }() default: break } } @@ -1934,6 +2147,10 @@ extension Telemetry: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementation guard case .healthMetrics(let v)? = self.variant else { preconditionFailure() } try visitor.visitSingularMessageField(value: v, fieldNumber: 7) }() + case .hostMetrics?: try { + guard case .hostMetrics(let v)? = self.variant else { preconditionFailure() } + try visitor.visitSingularMessageField(value: v, fieldNumber: 8) + }() case nil: break } try unknownFields.traverse(visitor: &visitor) diff --git a/protobufs b/protobufs index 816595c8..24c7a3d2 160000 --- a/protobufs +++ b/protobufs @@ -1 +1 @@ -Subproject commit 816595c8bbdfc3b4388e11348ccd043294d58705 +Subproject commit 24c7a3d287a4bd269ce191827e5dabd8ce8f57a7 diff --git a/scripts/gen_protos.sh b/scripts/gen_protos.sh index d07bc798..1ce1d1e9 100755 --- a/scripts/gen_protos.sh +++ b/scripts/gen_protos.sh @@ -5,6 +5,10 @@ if [ ! -x "$(which protoc)" ]; then brew install swift-protobuf fi +git submodule update --init --recursive + +git submodule foreach --recursive git pull origin master + protoc --proto_path=./protobufs --swift_opt=Visibility=Public --swift_out=./MeshtasticProtobufs/Sources ./protobufs/meshtastic/*.proto echo "Done generating the swift files from the proto files." From 453022efe7622ca0b1dc851743309db4ae291cd6 Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Wed, 28 May 2025 15:30:03 -0700 Subject: [PATCH 073/213] Finish removing legacy admin functionality --- Meshtastic/Views/Nodes/Helpers/NodeDetail.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Meshtastic/Views/Nodes/Helpers/NodeDetail.swift b/Meshtastic/Views/Nodes/Helpers/NodeDetail.swift index b2c97074..a420b8bf 100644 --- a/Meshtastic/Views/Nodes/Helpers/NodeDetail.swift +++ b/Meshtastic/Views/Nodes/Helpers/NodeDetail.swift @@ -486,7 +486,7 @@ struct NodeDetail: View { let connectedNode, self.bleManager.connectedPeripheral != nil { Section("Administration") { - if connectedNode.myInfo?.hasAdmin ?? false { + if UserDefaults.enableAdministration { Button { let adminMessageId = bleManager.requestDeviceMetadata( fromUser: connectedNode.user!, From c906bc2baf67a8f1434f75c6c197fee57f5a13c5 Mon Sep 17 00:00:00 2001 From: Ben Meadors Date: Thu, 29 May 2025 12:49:36 -0500 Subject: [PATCH 074/213] Switch is comprehensive now --- Meshtastic/Helpers/BLEManager.swift | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Meshtastic/Helpers/BLEManager.swift b/Meshtastic/Helpers/BLEManager.swift index 050958e0..5af2941e 100644 --- a/Meshtastic/Helpers/BLEManager.swift +++ b/Meshtastic/Helpers/BLEManager.swift @@ -978,6 +978,8 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate Logger.mesh.info("🕸️ MESH PACKET received for Power Stress App UNHANDLED \((try? decodedInfo.packet.jsonString()) ?? "JSON Decode Failure", privacy: .public)") case .reticulumTunnelApp: Logger.mesh.info("🕸️ MESH PACKET received for Reticulum Tunnel App UNHANDLED \((try? decodedInfo.packet.jsonString()) ?? "JSON Decode Failure", privacy: .public)") + case .keyVerificationApp: + Logger.mesh.warning("🕸️ MESH PACKET received for Key Verification App UNHANDLED \((try? decodedInfo.packet.jsonString()) ?? "JSON Decode Failure", privacy: .public)") } if decodedInfo.configCompleteID != 0 && decodedInfo.configCompleteID == configNonce { From f8728df6639b81c0f6f10cd64bba58b96957f4d3 Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Fri, 30 May 2025 12:37:14 -0700 Subject: [PATCH 075/213] Map report opt in plumbing --- Meshtastic.xcodeproj/project.pbxproj | 4 +- Meshtastic/Helpers/BLEManager.swift | 2 + .../Meshtastic.xcdatamodeld/.xccurrentversion | 2 +- .../contents | 506 ++++++++++++++++++ Meshtastic/Persistence/UpdateCoreData.swift | 1 + 5 files changed, 513 insertions(+), 2 deletions(-) create mode 100644 Meshtastic/Meshtastic.xcdatamodeld/MeshtasticDataModelV 52.xcdatamodel/contents diff --git a/Meshtastic.xcodeproj/project.pbxproj b/Meshtastic.xcodeproj/project.pbxproj index 877cd45a..ec788769 100644 --- a/Meshtastic.xcodeproj/project.pbxproj +++ b/Meshtastic.xcodeproj/project.pbxproj @@ -352,6 +352,7 @@ DD007BAD2AA4E91200F5FA12 /* MyInfoEntityExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MyInfoEntityExtension.swift; sourceTree = ""; }; DD007BAF2AA5981000F5FA12 /* NodeInfoEntityExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NodeInfoEntityExtension.swift; sourceTree = ""; }; DD05296F2B77F454008E44CD /* MeshtasticDataModelV 26.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = "MeshtasticDataModelV 26.xcdatamodel"; sourceTree = ""; }; + DD0836AB2DE7C7CB00A3A973 /* MeshtasticDataModelV 52.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = "MeshtasticDataModelV 52.xcdatamodel"; sourceTree = ""; }; DD0BE30C2CB785D8000BA445 /* MeshtasticDataModelV 46.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = "MeshtasticDataModelV 46.xcdatamodel"; sourceTree = ""; }; DD0BE30F2CB9FDC4000BA445 /* DetectionSensorEnums.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DetectionSensorEnums.swift; sourceTree = ""; }; DD0E20FF2B892E1300F2D100 /* MeshtasticDataModelV 28.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = "MeshtasticDataModelV 28.xcdatamodel"; sourceTree = ""; }; @@ -2001,6 +2002,7 @@ DD3CC6BA28E366DF00FA9159 /* Meshtastic.xcdatamodeld */ = { isa = XCVersionGroup; children = ( + DD0836AB2DE7C7CB00A3A973 /* MeshtasticDataModelV 52.xcdatamodel */, DD63CB4E2DD4FBEA00AFCAE2 /* MeshtasticDataModelV 51.xcdatamodel */, 233E99B32D84969500CC3A77 /* MeshtasticDataModelV 50.xcdatamodel */, 8D3F8A3D2D44B137009EAAA4 /* MeshtasticDataModelV 49.xcdatamodel */, @@ -2053,7 +2055,7 @@ DD5D0A9A2931AD6B00F7EA61 /* MeshtasticDataModelV2.xcdatamodel */, DD3CC6BB28E366DF00FA9159 /* MeshtasticDataModel.xcdatamodel */, ); - currentVersion = DD63CB4E2DD4FBEA00AFCAE2 /* MeshtasticDataModelV 51.xcdatamodel */; + currentVersion = DD0836AB2DE7C7CB00A3A973 /* MeshtasticDataModelV 52.xcdatamodel */; name = Meshtastic.xcdatamodeld; path = Meshtastic/Meshtastic.xcdatamodeld; sourceTree = ""; diff --git a/Meshtastic/Helpers/BLEManager.swift b/Meshtastic/Helpers/BLEManager.swift index 050958e0..933dc73d 100644 --- a/Meshtastic/Helpers/BLEManager.swift +++ b/Meshtastic/Helpers/BLEManager.swift @@ -978,6 +978,8 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate Logger.mesh.info("🕸️ MESH PACKET received for Power Stress App UNHANDLED \((try? decodedInfo.packet.jsonString()) ?? "JSON Decode Failure", privacy: .public)") case .reticulumTunnelApp: Logger.mesh.info("🕸️ MESH PACKET received for Reticulum Tunnel App UNHANDLED \((try? decodedInfo.packet.jsonString()) ?? "JSON Decode Failure", privacy: .public)") + case .keyVerificationApp: + Logger.mesh.info("🕸️ MESH PACKET received for Key Verification App UNHANDLED \((try? decodedInfo.packet.jsonString()) ?? "JSON Decode Failure", privacy: .public)") } if decodedInfo.configCompleteID != 0 && decodedInfo.configCompleteID == configNonce { diff --git a/Meshtastic/Meshtastic.xcdatamodeld/.xccurrentversion b/Meshtastic/Meshtastic.xcdatamodeld/.xccurrentversion index 1853a00f..160ee4b2 100644 --- a/Meshtastic/Meshtastic.xcdatamodeld/.xccurrentversion +++ b/Meshtastic/Meshtastic.xcdatamodeld/.xccurrentversion @@ -3,6 +3,6 @@ _XCCurrentVersionName - MeshtasticDataModelV 51.xcdatamodel + MeshtasticDataModelV 52.xcdatamodel diff --git a/Meshtastic/Meshtastic.xcdatamodeld/MeshtasticDataModelV 52.xcdatamodel/contents b/Meshtastic/Meshtastic.xcdatamodeld/MeshtasticDataModelV 52.xcdatamodel/contents new file mode 100644 index 00000000..c36266d8 --- /dev/null +++ b/Meshtastic/Meshtastic.xcdatamodeld/MeshtasticDataModelV 52.xcdatamodel/contents @@ -0,0 +1,506 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/Meshtastic/Persistence/UpdateCoreData.swift b/Meshtastic/Persistence/UpdateCoreData.swift index 53da7355..c53b2f73 100644 --- a/Meshtastic/Persistence/UpdateCoreData.swift +++ b/Meshtastic/Persistence/UpdateCoreData.swift @@ -1231,6 +1231,7 @@ func upsertMqttModuleConfigPacket(config: ModuleConfig.MQTTConfig, nodeNum: Int6 newMQTTConfig.jsonEnabled = config.jsonEnabled newMQTTConfig.tlsEnabled = config.tlsEnabled newMQTTConfig.mapReportingEnabled = config.mapReportingEnabled + newMQTTConfig.mapReportingShouldReportLocation = config.mapReportSettings.shouldReportLocation newMQTTConfig.mapPositionPrecision = Int32(config.mapReportSettings.positionPrecision) newMQTTConfig.mapPublishIntervalSecs = Int32(config.mapReportSettings.publishIntervalSecs) fetchedNode[0].mqttConfig = newMQTTConfig From 701a06a02d9c6a4c6024714079473891116666b4 Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Sun, 1 Jun 2025 07:00:06 -0700 Subject: [PATCH 076/213] Bump minimum firmare version to match android --- Meshtastic/Helpers/BLEManager.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Meshtastic/Helpers/BLEManager.swift b/Meshtastic/Helpers/BLEManager.swift index 5af2941e..04e32452 100644 --- a/Meshtastic/Helpers/BLEManager.swift +++ b/Meshtastic/Helpers/BLEManager.swift @@ -27,7 +27,7 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate @Published var automaticallyReconnect: Bool = true @Published var mqttProxyConnected: Bool = false @Published var mqttError: String = "" - public var minimumVersion = "2.3.15" + public var minimumVersion = "2.5.14" public var connectedVersion: String public var isConnecting: Bool = false public var isConnected: Bool = false From 7128aface42fea9e0e1314cd310548c8cbe6940d Mon Sep 17 00:00:00 2001 From: Brent Petit Date: Sat, 31 May 2025 20:42:09 -0500 Subject: [PATCH 077/213] App option for purging nodes not heard in certain amount of time --- Localizable.xcstrings | 34 +++++++++++++++++++++ Meshtastic/Helpers/BLEManager.swift | 20 +++++++----- Meshtastic/Helpers/MeshPackets.swift | 10 ++++-- Meshtastic/Persistence/UpdateCoreData.swift | 32 +++++++++++++++++++ Meshtastic/Views/Settings/AppSettings.swift | 32 +++++++++++++++++++ 5 files changed, 118 insertions(+), 10 deletions(-) diff --git a/Localizable.xcstrings b/Localizable.xcstrings index eec7ee0d..c0599a50 100644 --- a/Localizable.xcstrings +++ b/Localizable.xcstrings @@ -1378,6 +1378,12 @@ } } } + }, + "0" : { + + }, + "1" : { + }, "1 byte" : { "localizations" : { @@ -1652,6 +1658,9 @@ } } } + }, + "180" : { + }, "256 bit" : { "localizations" : { @@ -2459,6 +2468,28 @@ } } }, + "After %lld Days" : { + "localizations" : { + "en" : { + "variations" : { + "plural" : { + "one" : { + "stringUnit" : { + "state" : "translated", + "value" : "After %lld Day" + } + }, + "other" : { + "stringUnit" : { + "state" : "new", + "value" : "After %lld Days" + } + } + } + } + } + } + }, "After config values save the node will reboot." : { "localizations" : { "de" : { @@ -6379,6 +6410,9 @@ } } } + }, + "Clear Stale Nodes" : { + }, "Client" : { "localizations" : { diff --git a/Meshtastic/Helpers/BLEManager.swift b/Meshtastic/Helpers/BLEManager.swift index 050958e0..d76ffcfa 100644 --- a/Meshtastic/Helpers/BLEManager.swift +++ b/Meshtastic/Helpers/BLEManager.swift @@ -36,6 +36,7 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate var timeoutTimer: Timer? var timeoutTimerCount = 0 var positionTimer: Timer? + var maintenenceTimer: Timer? let mqttManager = MqttClientProxyManager.shared var wantRangeTestPackets = false var wantStoreAndForwardPackets = false @@ -52,6 +53,7 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate let FROMNUM_UUID = CBUUID(string: "0xED9DA18C-A800-4F66-A670-AA7547E34453") let LEGACY_LOGRADIO_UUID = CBUUID(string: "0x6C6FD238-78FA-436B-AACF-15C5BE1EF2E2") let LOGRADIO_UUID = CBUUID(string: "0x5a3d6e49-06e6-4423-9944-e9de8cdf9547") + @AppStorage("purgeStaleNodeDays") var purgeStaleNodeDays: Double = 0 // MARK: init private override init() { @@ -68,13 +70,17 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate } private init(appState: AppState, context: NSManagedObjectContext) { - self.appState = appState - self.context = context - self.lastConnectionError = "" - self.connectedVersion = "0.0.0" - super.init() - centralManager = CBCentralManager(delegate: self, queue: nil) - mqttManager.delegate = self + self.appState = appState + self.context = context + self.lastConnectionError = "" + self.connectedVersion = "0.0.0" + super.init() + centralManager = CBCentralManager(delegate: self, queue: nil) + mqttManager.delegate = self + // Run clearStaleNodes every 10 minutes + maintenenceTimer = Timer.scheduledTimer(withTimeInterval: 600, repeats: true, block: { _ in + clearStaleNodes(nodeExpireDays: Int(self.purgeStaleNodeDays), context: self.context) + }) } // MARK: Scanning for BLE Devices diff --git a/Meshtastic/Helpers/MeshPackets.swift b/Meshtastic/Helpers/MeshPackets.swift index fe290aa0..6149b582 100644 --- a/Meshtastic/Helpers/MeshPackets.swift +++ b/Meshtastic/Helpers/MeshPackets.swift @@ -292,9 +292,13 @@ func nodeInfoPacket (nodeInfo: NodeInfo, channel: UInt32, context: NSManagedObje newTelemetries.append(telemetry) newNode.telemetries? = NSOrderedSet(array: newTelemetries) } - - newNode.firstHeard = Date(timeIntervalSince1970: TimeInterval(Int64(nodeInfo.lastHeard))) - newNode.lastHeard = Date(timeIntervalSince1970: TimeInterval(Int64(nodeInfo.lastHeard))) + if nodeInfo.lastHeard > 0 { + newNode.firstHeard = Date(timeIntervalSince1970: TimeInterval(Int64(nodeInfo.lastHeard))) + newNode.lastHeard = Date(timeIntervalSince1970: TimeInterval(Int64(nodeInfo.lastHeard))) + } else { + newNode.firstHeard = Date() + newNode.lastHeard = Date() + } newNode.snr = nodeInfo.snr if nodeInfo.hasUser { diff --git a/Meshtastic/Persistence/UpdateCoreData.swift b/Meshtastic/Persistence/UpdateCoreData.swift index 53da7355..ac81a25f 100644 --- a/Meshtastic/Persistence/UpdateCoreData.swift +++ b/Meshtastic/Persistence/UpdateCoreData.swift @@ -8,6 +8,38 @@ import CoreData import MeshtasticProtobufs import OSLog +public func clearStaleNodes(nodeExpireDays: Int, context: NSManagedObjectContext) { + var nodeExpireTime: TimeInterval { + return TimeInterval(-nodeExpireDays * 86400) + } + + if nodeExpireDays == 0 { + // Purge Disabled + Logger.data.info("💾 [NodeInfoEntity] Skip clearing stale nodes") + return + } + let fetchRequest = NSFetchRequest(entityName: "NodeInfoEntity") + fetchRequest.predicate = NSPredicate(format: "lastHeard < %@ and favorite == false and ignored == false", + NSDate(timeIntervalSinceNow: nodeExpireTime)) + + let batchDeleteRequest = NSBatchDeleteRequest(fetchRequest: fetchRequest) + batchDeleteRequest.resultType = .resultTypeCount + + do { + Logger.data.info("💾 [NodeInfoEntity] Clearing nodes older than \(nodeExpireDays) days") + if let batchDeleteResult = try context.execute(batchDeleteRequest) as? NSBatchDeleteResult { + try context.save() + let deletedNodes = batchDeleteResult.result as? Int ?? 0 + Logger.data.info("💾 [NodeInfoEntity] Cleared \(deletedNodes) stale nodes") + } else { + Logger.data.error("💥 [NodeInfoEntity] bad delete results") + } + } catch { + context.rollback() + Logger.data.error("💥 [NodeInfoEntity] Error deleting stale nodes") + } +} + public func clearPax(destNum: Int64, context: NSManagedObjectContext) -> Bool { let fetchNodeInfoRequest = NodeInfoEntity.fetchRequest() diff --git a/Meshtastic/Views/Settings/AppSettings.swift b/Meshtastic/Views/Settings/AppSettings.swift index e16fb31b..c64e049c 100644 --- a/Meshtastic/Views/Settings/AppSettings.swift +++ b/Meshtastic/Views/Settings/AppSettings.swift @@ -11,6 +11,8 @@ struct AppSettings: View { @State var totalDownloadedTileSize = "" @State private var isPresentingCoreDataResetConfirm = false @State private var isPresentingDeleteMapTilesConfirm = false + @State private var purgeStaleNodes: Bool = false + @AppStorage("purgeStaleNodeDays") private var purgeStaleNodeDays: Double = 0 @AppStorage("environmentEnableWeatherKit") private var environmentEnableWeatherKit: Bool = true @AppStorage("enableAdministration") private var enableAdministration: Bool = false var body: some View { @@ -40,6 +42,36 @@ struct AppSettings: View { } } Section(header: Text("App Data")) { + Toggle(isOn: $purgeStaleNodes ) { + Label { + Text("Clear Stale Nodes") + } icon: { + Image(systemName: "list.bullet.circle") + } + } + .onFirstAppear { + purgeStaleNodes = purgeStaleNodeDays > 0 + Logger.services.info("ℹ️ Purge Stale Nodes toggle initialized to \(purgeStaleNodes)") + } + .onChange(of: purgeStaleNodes) { _, newValue in + purgeStaleNodeDays = purgeStaleNodeDays > 0 ? purgeStaleNodeDays : 7 + purgeStaleNodeDays = newValue ? purgeStaleNodeDays : 0 + Logger.services.info("ℹ️ Purge Stale Nodes changed to \(purgeStaleNodeDays)") + } + .toggleStyle(SwitchToggleStyle(tint: .accentColor)) + + .listRowSeparator(purgeStaleNodes ? .hidden : .visible) + if purgeStaleNodes { + VStack(alignment: .leading) { + Text(String(localized: "After \(Int(purgeStaleNodeDays)) Days")) + Slider(value: $purgeStaleNodeDays, in: 1...180, step: 1) { + } minimumValueLabel: { + Text("1") + } maximumValueLabel: { + Text("180") + } + } + } Button { isPresentingCoreDataResetConfirm = true } label: { From c09291e1b2f0c2b548c8bdc3ce2fca25d41abec3 Mon Sep 17 00:00:00 2001 From: Benjamin Faershtein <119711889+RCGV1@users.noreply.github.com> Date: Wed, 4 Jun 2025 09:25:30 -0700 Subject: [PATCH 078/213] Added a button to change waypoint to your location --- .../CoreData/WaypointEntityExtension.swift | 28 ++++++++++++------- .../Nodes/Helpers/Map/WaypointForm.swift | 8 ++++++ 2 files changed, 26 insertions(+), 10 deletions(-) diff --git a/Meshtastic/Extensions/CoreData/WaypointEntityExtension.swift b/Meshtastic/Extensions/CoreData/WaypointEntityExtension.swift index 2f538b62..240e6d76 100644 --- a/Meshtastic/Extensions/CoreData/WaypointEntityExtension.swift +++ b/Meshtastic/Extensions/CoreData/WaypointEntityExtension.swift @@ -14,9 +14,6 @@ extension WaypointEntity { static func allWaypointssFetchRequest() -> NSFetchRequest { let request: NSFetchRequest = WaypointEntity.fetchRequest() request.fetchLimit = 50 - // request.fetchBatchSize = 1 - // request.returnsObjectsAsFaults = false - // request.includesSubentities = true request.returnsDistinctResults = true request.sortDescriptors = [NSSortDescriptor(key: "name", ascending: false)] request.predicate = NSPredicate(format: "expire == nil || expire >= %@", Date() as NSDate) @@ -24,7 +21,6 @@ extension WaypointEntity { } var latitude: Double? { - let d = Double(latitudeI) if d == 0 { return 0 @@ -33,7 +29,6 @@ extension WaypointEntity { } var longitude: Double? { - let d = Double(longitudeI) if d == 0 { return 0 @@ -46,7 +41,7 @@ extension WaypointEntity { let coord = CLLocationCoordinate2D(latitude: latitude!, longitude: longitude!) return coord } else { - return nil + return nil } } @@ -60,16 +55,29 @@ extension WaypointEntity { } extension WaypointEntity: MKAnnotation { - public var coordinate: CLLocationCoordinate2D { waypointCoordinate ?? LocationsHandler.DefaultLocation } - public var title: String? { name ?? "Dropped Pin" } + @MainActor + public var coordinate: CLLocationCoordinate2D { + get { + waypointCoordinate ?? LocationsHandler.currentLocation + } + set { + latitudeI = Int32(newValue.latitude * 1e7) + longitudeI = Int32(newValue.longitude * 1e7) + } + } + + public var title: String? { + name ?? "Dropped Pin" + } + public var subtitle: String? { (longDescription ?? "") + String(expire != nil ? "\n⌛ Expires \(String(describing: expire?.formatted()))" : "") + - String(locked > 0 ? "\n🔒 Locked" : "") } + String(locked > 0 ? "\n🔒 Locked" : "") + } } struct WaypointCoordinate: Identifiable { - let id: UUID let coordinate: CLLocationCoordinate2D? let waypointId: Int64 diff --git a/Meshtastic/Views/Nodes/Helpers/Map/WaypointForm.swift b/Meshtastic/Views/Nodes/Helpers/Map/WaypointForm.swift index ad342cbc..57320259 100644 --- a/Meshtastic/Views/Nodes/Helpers/Map/WaypointForm.swift +++ b/Meshtastic/Views/Nodes/Helpers/Map/WaypointForm.swift @@ -47,6 +47,14 @@ struct WaypointForm: View { .textSelection(.enabled) .foregroundColor(.secondary) .font(.caption) + + Button { + let currentLoc = LocationsHandler.currentLocation + waypoint.coordinate.longitude = currentLoc.longitude + waypoint.coordinate.latitude = currentLoc.latitude + } label: { + Image(systemName: "location") + } } HStack { if waypoint.coordinate.latitude != 0 && waypoint.coordinate.longitude != 0 { From 8d65985521ebf2c81584f4c1903b4281eed86c64 Mon Sep 17 00:00:00 2001 From: Benjamin Faershtein <119711889+RCGV1@users.noreply.github.com> Date: Wed, 4 Jun 2025 15:56:17 -0700 Subject: [PATCH 079/213] Updated send waypoint intent --- Localizable.xcstrings | 16 +++++++ .../AppIntents/SendWaypointIntent.swift | 48 ++++++++++++++----- 2 files changed, 51 insertions(+), 13 deletions(-) diff --git a/Localizable.xcstrings b/Localizable.xcstrings index eec7ee0d..7f283f03 100644 --- a/Localizable.xcstrings +++ b/Localizable.xcstrings @@ -11388,6 +11388,9 @@ } } } + }, + "Expiration" : { + }, "Expire" : { "localizations" : { @@ -15747,6 +15750,12 @@ } } } + }, + "Latitude in degrees (e.g., 37.7749)" : { + + }, + "Latitude must be between -90 and 90 degrees" : { + }, "LED Heartbeat" : { "localizations" : { @@ -16055,6 +16064,7 @@ } }, "Location" : { + "extractionState" : "stale", "localizations" : { "de" : { "stringUnit" : { @@ -16493,6 +16503,12 @@ } } } + }, + "Longitude in degrees (e.g., -122.4194)" : { + + }, + "Longitude must be between -180 and 180 degrees" : { + }, "LoRa" : { "localizations" : { diff --git a/Meshtastic/AppIntents/SendWaypointIntent.swift b/Meshtastic/AppIntents/SendWaypointIntent.swift index 4352c548..e41732db 100644 --- a/Meshtastic/AppIntents/SendWaypointIntent.swift +++ b/Meshtastic/AppIntents/SendWaypointIntent.swift @@ -11,6 +11,8 @@ import AppIntents import MeshtasticProtobufs struct SendWaypointIntent: AppIntent { + + var defaultDate = Date.now.addingTimeInterval(60 * 480) static var title = LocalizedStringResource("Send a Waypoint") @@ -23,13 +25,24 @@ struct SendWaypointIntent: AppIntent { @Parameter(title: "Emoji", default: "📍") var emojiParameter: String? - @Parameter(title: "Location") - var locationParameter: CLPlacemark + // Replace CLPlacemark with latitude and longitude parameters + @Parameter(title: "Latitude", description: "Latitude in degrees (e.g., 37.7749)") + var latitudeParameter: Double + + @Parameter(title: "Longitude", description: "Longitude in degrees (e.g., -122.4194)") + var longitudeParameter: Double + + @Parameter(title: "Locked", default: false) + var isLocked: Bool + + @Parameter(title: "Expiration") + var expiration: Date? func perform() async throws -> some IntentResult { if !BLEManager.shared.isConnected { throw AppIntentErrors.AppIntentError.notConnected } + // Provide default values if parameters are nil let name = nameParameter ?? "Dropped Pin" let description = descriptionParameter ?? "" @@ -50,24 +63,35 @@ struct SendWaypointIntent: AppIntent { throw $emojiParameter.needsValueError("Must be a single emoji") } + // Validate latitude and longitude + guard abs(latitudeParameter) <= 90 else { + throw $latitudeParameter.needsValueError("Latitude must be between -90 and 90 degrees") + } + guard abs(longitudeParameter) <= 180 else { + throw $longitudeParameter.needsValueError("Longitude must be between -180 and 180 degrees") + } + var newWaypoint = Waypoint() - if let latitude = locationParameter.location?.coordinate.latitude { - newWaypoint.latitudeI = Int32(latitude * 10_000_000) - } - - if let longitude = locationParameter.location?.coordinate.longitude { - newWaypoint.longitudeI = Int32(longitude * 10_000_000) - } + // Set latitude and longitude directly + newWaypoint.latitudeI = Int32(latitudeParameter * 10_000_000) + newWaypoint.longitudeI = Int32(longitudeParameter * 10_000_000) newWaypoint.id = UInt32.random(in: UInt32(UInt8.max).. Bool { - // This regex pattern is for matching a single emoji let emojiPattern = "^([\\p{So}\\p{Cn}])$" let regex = try? NSRegularExpression(pattern: emojiPattern, options: []) let matches = regex?.matches(in: emoji, options: [], range: NSRange(location: 0, length: emoji.utf16.count)) - return matches?.count == 1 } } From bd666a742c5c6c75d0f796496b32359f7d24e27a Mon Sep 17 00:00:00 2001 From: Benjamin Faershtein <119711889+RCGV1@users.noreply.github.com> Date: Thu, 5 Jun 2025 08:24:17 -0700 Subject: [PATCH 080/213] More graceful failures --- Localizable.xcstrings | 3 +++ .../CoreData/WaypointEntityExtension.swift | 2 +- Meshtastic/Helpers/BLEManager.swift | 2 +- .../Nodes/Helpers/Map/WaypointForm.swift | 19 ++++++++++++++++--- 4 files changed, 21 insertions(+), 5 deletions(-) diff --git a/Localizable.xcstrings b/Localizable.xcstrings index 7f283f03..e6a7809f 100644 --- a/Localizable.xcstrings +++ b/Localizable.xcstrings @@ -34355,6 +34355,9 @@ } } } + }, + "Waypoiny Failed to Send" : { + }, "Weather Conditions" : { "localizations" : { diff --git a/Meshtastic/Extensions/CoreData/WaypointEntityExtension.swift b/Meshtastic/Extensions/CoreData/WaypointEntityExtension.swift index 240e6d76..4f2923eb 100644 --- a/Meshtastic/Extensions/CoreData/WaypointEntityExtension.swift +++ b/Meshtastic/Extensions/CoreData/WaypointEntityExtension.swift @@ -58,7 +58,7 @@ extension WaypointEntity: MKAnnotation { @MainActor public var coordinate: CLLocationCoordinate2D { get { - waypointCoordinate ?? LocationsHandler.currentLocation + waypointCoordinate ?? LocationsHandler.DefaultLocation } set { latitudeI = Int32(newValue.latitude * 1e7) diff --git a/Meshtastic/Helpers/BLEManager.swift b/Meshtastic/Helpers/BLEManager.swift index 050958e0..59571549 100644 --- a/Meshtastic/Helpers/BLEManager.swift +++ b/Meshtastic/Helpers/BLEManager.swift @@ -1183,7 +1183,7 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate } public func sendWaypoint(waypoint: Waypoint) -> Bool { - if waypoint.latitudeI == 373346000 && waypoint.longitudeI == -1220090000 { + if waypoint.latitudeI == 0 && waypoint.longitudeI == 0 { return false } var success = false diff --git a/Meshtastic/Views/Nodes/Helpers/Map/WaypointForm.swift b/Meshtastic/Views/Nodes/Helpers/Map/WaypointForm.swift index 57320259..e813b854 100644 --- a/Meshtastic/Views/Nodes/Helpers/Map/WaypointForm.swift +++ b/Meshtastic/Views/Nodes/Helpers/Map/WaypointForm.swift @@ -30,6 +30,7 @@ struct WaypointForm: View { @State private var lockedTo: Int64 = 0 @State private var detents: Set = [.medium, .fraction(0.85)] @State private var selectedDetent: PresentationDetent = .medium + @State private var waypointFailedAlert: Bool = false var body: some View { NavigationStack { @@ -47,7 +48,7 @@ struct WaypointForm: View { .textSelection(.enabled) .foregroundColor(.secondary) .font(.caption) - + Button { let currentLoc = LocationsHandler.currentLocation waypoint.coordinate.longitude = currentLoc.longitude @@ -80,6 +81,7 @@ struct WaypointForm: View { name = String(name.dropLast()) totalBytes = name.utf8.count } + waypoint.name = name.count > 0 ? name : "Dropped Pin" } } HStack { @@ -175,8 +177,8 @@ struct WaypointForm: View { if bleManager.sendWaypoint(waypoint: newWaypoint) { dismiss() } else { - dismiss() Logger.mesh.warning("Send waypoint failed") + waypointFailedAlert = true } } else { Logger.mesh.warning("Send waypoint failed, node not connected") @@ -241,8 +243,8 @@ struct WaypointForm: View { } dismiss() } else { - dismiss() Logger.mesh.warning("Send waypoint failed") + waypointFailedAlert = true } }) } @@ -376,6 +378,17 @@ struct WaypointForm: View { } } } + .alert("Waypoiny Failed to Send", isPresented: $waypointFailedAlert) { + Button("OK", role: .cancel) { + bleManager.context.delete(waypoint) + do { + try bleManager.context.save() + } catch { + bleManager.context.rollback() + } + dismiss() + } + } .onDisappear { if waypoint.id == 0 { // New, unsent waypoint created by the user: delete it From 8ef6d0cea68497425e5933a8951247bb915a2edd Mon Sep 17 00:00:00 2001 From: Benjamin Faershtein <119711889+RCGV1@users.noreply.github.com> Date: Thu, 5 Jun 2025 08:28:04 -0700 Subject: [PATCH 081/213] fix --- Meshtastic/Views/Nodes/Helpers/Map/WaypointForm.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Meshtastic/Views/Nodes/Helpers/Map/WaypointForm.swift b/Meshtastic/Views/Nodes/Helpers/Map/WaypointForm.swift index e813b854..caee990f 100644 --- a/Meshtastic/Views/Nodes/Helpers/Map/WaypointForm.swift +++ b/Meshtastic/Views/Nodes/Helpers/Map/WaypointForm.swift @@ -378,7 +378,7 @@ struct WaypointForm: View { } } } - .alert("Waypoiny Failed to Send", isPresented: $waypointFailedAlert) { + .alert("Waypoint Failed to Send", isPresented: $waypointFailedAlert) { Button("OK", role: .cancel) { bleManager.context.delete(waypoint) do { From 7e0c0b834791533d29c48cfb07b2b3159c85fe47 Mon Sep 17 00:00:00 2001 From: Benjamin Faershtein <119711889+RCGV1@users.noreply.github.com> Date: Thu, 5 Jun 2025 08:28:28 -0700 Subject: [PATCH 082/213] Fix waypoiny --- Localizable.xcstrings | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Localizable.xcstrings b/Localizable.xcstrings index e6a7809f..ffeaff93 100644 --- a/Localizable.xcstrings +++ b/Localizable.xcstrings @@ -34356,7 +34356,7 @@ } } }, - "Waypoiny Failed to Send" : { + "Waypoint Failed to Send" : { }, "Weather Conditions" : { @@ -35340,4 +35340,4 @@ } }, "version" : "1.0" -} \ No newline at end of file +} From e7055a2768285c6c0996760066a490a4f3076360 Mon Sep 17 00:00:00 2001 From: Benjamin Faershtein <119711889+RCGV1@users.noreply.github.com> Date: Thu, 5 Jun 2025 08:29:03 -0700 Subject: [PATCH 083/213] Update Meshtastic/AppIntents/SendWaypointIntent.swift Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- Meshtastic/AppIntents/SendWaypointIntent.swift | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/Meshtastic/AppIntents/SendWaypointIntent.swift b/Meshtastic/AppIntents/SendWaypointIntent.swift index e41732db..fb0f97c3 100644 --- a/Meshtastic/AppIntents/SendWaypointIntent.swift +++ b/Meshtastic/AppIntents/SendWaypointIntent.swift @@ -89,7 +89,11 @@ struct SendWaypointIntent: AppIntent { } if isLocked { - newWaypoint.lockedTo = UInt32(BLEManager.shared.connectedPeripheral!.num) + if let connectedPeripheral = BLEManager.shared.connectedPeripheral { + newWaypoint.lockedTo = UInt32(connectedPeripheral.num) + } else { + throw AppIntentErrors.AppIntentError.notConnected + } } if !BLEManager.shared.sendWaypoint(waypoint: newWaypoint) { From a16c7ade27f97fac491659ac44b6d84f6c1a97bb Mon Sep 17 00:00:00 2001 From: Benjamin Faershtein <119711889+RCGV1@users.noreply.github.com> Date: Thu, 5 Jun 2025 08:30:15 -0700 Subject: [PATCH 084/213] Accessibility Label --- Meshtastic/Views/Nodes/Helpers/Map/WaypointForm.swift | 1 + 1 file changed, 1 insertion(+) diff --git a/Meshtastic/Views/Nodes/Helpers/Map/WaypointForm.swift b/Meshtastic/Views/Nodes/Helpers/Map/WaypointForm.swift index caee990f..61bc8b7a 100644 --- a/Meshtastic/Views/Nodes/Helpers/Map/WaypointForm.swift +++ b/Meshtastic/Views/Nodes/Helpers/Map/WaypointForm.swift @@ -56,6 +56,7 @@ struct WaypointForm: View { } label: { Image(systemName: "location") } + .accessibilityLabel("Set to current location") } HStack { if waypoint.coordinate.latitude != 0 && waypoint.coordinate.longitude != 0 { From 784d3ab7f48eb240754218141442b015813b764b Mon Sep 17 00:00:00 2001 From: Benjamin Faershtein <119711889+RCGV1@users.noreply.github.com> Date: Thu, 5 Jun 2025 09:23:51 -0700 Subject: [PATCH 085/213] Fixed lock waypoint logic --- Localizable.xcstrings | 11 +++++++---- Meshtastic/Views/Nodes/Helpers/Map/WaypointForm.swift | 4 ++-- 2 files changed, 9 insertions(+), 6 deletions(-) diff --git a/Localizable.xcstrings b/Localizable.xcstrings index ffeaff93..b5d0c59d 100644 --- a/Localizable.xcstrings +++ b/Localizable.xcstrings @@ -27888,6 +27888,9 @@ } } } + }, + "Set to current location" : { + }, "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" : { @@ -34269,6 +34272,9 @@ } } } + }, + "Waypoint Failed to Send" : { + }, "Waypoint Options" : { "localizations" : { @@ -34355,9 +34361,6 @@ } } } - }, - "Waypoint Failed to Send" : { - }, "Weather Conditions" : { "localizations" : { @@ -35340,4 +35343,4 @@ } }, "version" : "1.0" -} +} \ No newline at end of file diff --git a/Meshtastic/Views/Nodes/Helpers/Map/WaypointForm.swift b/Meshtastic/Views/Nodes/Helpers/Map/WaypointForm.swift index 61bc8b7a..5279dfe2 100644 --- a/Meshtastic/Views/Nodes/Helpers/Map/WaypointForm.swift +++ b/Meshtastic/Views/Nodes/Helpers/Map/WaypointForm.swift @@ -267,8 +267,8 @@ struct WaypointForm: View { Text(waypoint.name ?? "?") .font(.largeTitle) Spacer() - if waypoint.locked > 0 { - Image(systemName: "lock.fill" ) + if waypoint.locked > 0 && waypoint.locked != UInt32(BLEManager.shared.connectedPeripheral?.num ?? 0) { + Image(systemName: "lock.fill") .font(.largeTitle) } else { Button { From 339ecb3aceb6741bd6b36d1ede79e07492daa31b Mon Sep 17 00:00:00 2001 From: Benjamin Faershtein <119711889+RCGV1@users.noreply.github.com> Date: Sat, 7 Jun 2025 14:58:06 -0700 Subject: [PATCH 086/213] Actually save isClientMuted to the node and switch channelList to a fetch request --- .../CoreData/ChannelEntityExtension.swift | 1 + Meshtastic/Views/Messages/ChannelList.swift | 13 +++++++++++-- 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/Meshtastic/Extensions/CoreData/ChannelEntityExtension.swift b/Meshtastic/Extensions/CoreData/ChannelEntityExtension.swift index 57babf4a..c85eef4a 100644 --- a/Meshtastic/Extensions/CoreData/ChannelEntityExtension.swift +++ b/Meshtastic/Extensions/CoreData/ChannelEntityExtension.swift @@ -32,6 +32,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 return channel } } diff --git a/Meshtastic/Views/Messages/ChannelList.swift b/Meshtastic/Views/Messages/ChannelList.swift index 1123c4ab..61f4fe00 100644 --- a/Meshtastic/Views/Messages/ChannelList.swift +++ b/Meshtastic/Views/Messages/ChannelList.swift @@ -26,6 +26,12 @@ struct ChannelList: View { var restrictedChannels = ["gpio", "mqtt", "serial", "admin"] + @FetchRequest( + sortDescriptors: [NSSortDescriptor(keyPath: \ChannelEntity.index, ascending: true)], + predicate: nil, + animation: .default + ) private var channels: FetchedResults + @ViewBuilder private func makeChannelRow( myInfo: MyInfoEntity, @@ -87,6 +93,9 @@ struct ChannelList: View { .foregroundColor(.secondary) } } + if channel.mute { + Image(systemName: "bell.slash") + } } if channel.allPrivateMessages.count > 0 { @@ -103,7 +112,7 @@ struct ChannelList: View { var body: some View { VStack { // Display Contacts for the rest of the non admin channels - if let node, let myInfo = node.myInfo, let channels = myInfo.channels?.array as? [ChannelEntity] { + if let node, let myInfo = node.myInfo { List(selection: $channelSelection) { ForEach(channels) { (channel: ChannelEntity) in if !restrictedChannels.contains(channel.name?.lowercased() ?? "") { @@ -119,7 +128,7 @@ struct ChannelList: View { } } Button { - channel.mute = !channel.mute + channel.mute.toggle() do { let adminMessageId = bleManager.saveChannel(channel: channel.protoBuf, fromUser: node.user!, toUser: node.user!) if adminMessageId > 0 { From 807c747182e8e3e9c76dcd8779b91c1f0bca8d95 Mon Sep 17 00:00:00 2001 From: Benjamin Faershtein <119711889+RCGV1@users.noreply.github.com> Date: Tue, 10 Jun 2025 13:35:48 -0700 Subject: [PATCH 087/213] fix filter --- Meshtastic/Views/Messages/UserList.swift | 409 +++++++++++------------ 1 file changed, 196 insertions(+), 213 deletions(-) diff --git a/Meshtastic/Views/Messages/UserList.swift b/Meshtastic/Views/Messages/UserList.swift index a74e3fd2..45f922e9 100644 --- a/Meshtastic/Views/Messages/UserList.swift +++ b/Meshtastic/Views/Messages/UserList.swift @@ -21,7 +21,6 @@ struct UserList: View { @State private var isPkiEncrypted = false @State private var isFavorite = false @State private var isIgnored = false - @State private var isUnmessagable = false @State private var isEnvironment = false @State private var distanceFilter = false @State private var maxDistance: Double = 800000 @@ -40,18 +39,6 @@ struct UserList: View { roleFilter ]} - @FetchRequest( - sortDescriptors: [NSSortDescriptor(key: "lastMessage", ascending: false), - NSSortDescriptor(key: "userNode.favorite", ascending: false), - NSSortDescriptor(key: "pkiEncrypted", ascending: false), - NSSortDescriptor(key: "userNode.lastHeard", ascending: false), - NSSortDescriptor(key: "longName", ascending: true)], - predicate: NSPredicate( - format: "userNode.ignored == NO AND unmessagable = NO" - ), animation: .spring - ) - var users: FetchedResults - @Binding var node: NodeInfoEntity? @Binding var userSelection: UserEntity? @@ -61,196 +48,162 @@ struct UserList: View { let localeDateFormat = DateFormatter.dateFormat(fromTemplate: "yyMMdd", options: 0, locale: Locale.current) let dateFormatString = (localeDateFormat ?? "MM/dd/YY") VStack { - List(users, selection: $userSelection) { (user: UserEntity) in - let mostRecent = user.messageList.last - 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 - if user.num != bleManager.connectedPeripheral?.num ?? 0 { - NavigationLink(value: user) { - ZStack { - Image(systemName: "circle.fill") - .opacity(user.unreadMessages > 0 ? 1 : 0) - .font(.system(size: 10)) - .foregroundColor(.accentColor) - .brightness(0.2) - } + FilteredUserList( + searchText: searchText, + viaLora: viaLora, + viaMqtt: viaMqtt, + isOnline: isOnline, + isPkiEncrypted: isPkiEncrypted, + isFavorite: isFavorite, + isIgnored: isIgnored, + isUnmessagable: false, + isEnvironment: isEnvironment, + distanceFilter: distanceFilter, + maxDistance: maxDistance, + hopsAway: hopsAway, + roleFilter: roleFilter, + deviceRoles: deviceRoles, + userSelection: $userSelection + ) { users in + List(users, selection: $userSelection) { (user: UserEntity) in + let mostRecent = user.messageList.last + 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 + if user.num != bleManager.connectedPeripheral?.num ?? 0 { + NavigationLink(value: user) { + ZStack { + Image(systemName: "circle.fill") + .opacity(user.unreadMessages > 0 ? 1 : 0) + .font(.system(size: 10)) + .foregroundColor(.accentColor) + .brightness(0.2) + } - CircleText(text: user.shortName ?? "?", color: Color(UIColor(hex: UInt32(user.num)))) + CircleText(text: user.shortName ?? "?", color: Color(UIColor(hex: UInt32(user.num)))) - VStack(alignment: .leading) { - HStack { - if user.pkiEncrypted { - if !user.keyMatch { - /// Public Key on the User and the Public Key on the Last Message don't match - Image(systemName: "key.slash") - .foregroundColor(.red) + VStack(alignment: .leading) { + HStack { + if user.pkiEncrypted { + if !user.keyMatch { + /// Public Key on the User and the Public Key on the Last Message don't match + Image(systemName: "key.slash") + .foregroundColor(.red) + } else { + Image(systemName: "lock.fill") + .foregroundColor(.green) + } } else { - Image(systemName: "lock.fill") - .foregroundColor(.green) + Image(systemName: "lock.open.fill") + .foregroundColor(.yellow) + } + Text(user.longName ?? "Unknown".localized) + .font(.headline) + .allowsTightening(true) + Spacer() + if user.userNode?.favorite ?? false { + Image(systemName: "star.fill") + .foregroundColor(.yellow) + } + if user.messageList.count > 0 { + if lastMessageDay == currentDay { + Text(lastMessageTime, style: .time ) + .font(.footnote) + .foregroundColor(.secondary) + } else if lastMessageDay == (currentDay - 1) { + Text("Yesterday") + .font(.footnote) + .foregroundColor(.secondary) + } else if lastMessageDay < (currentDay - 1) && lastMessageDay > (currentDay - 5) { + Text(lastMessageTime.formattedDate(format: dateFormatString)) + .font(.footnote) + .foregroundColor(.secondary) + } else if lastMessageDay < (currentDay - 1800) { + Text(lastMessageTime.formattedDate(format: dateFormatString)) + .font(.footnote) + .foregroundColor(.secondary) + } + } + } + + if user.messageList.count > 0 { + HStack(alignment: .top) { + Text("\(mostRecent != nil ? mostRecent!.messagePayload! : " ")") + .font(.footnote) + .foregroundColor(.secondary) + } + } + } + } + .frame(height: 62) + .contextMenu { + Button { + if node != nil && !(user.userNode?.favorite ?? false) { + let success = bleManager.setFavoriteNode(node: user.userNode!, connectedNodeNum: Int64(node!.num)) + if success { + user.userNode?.favorite = !(user.userNode?.favorite ?? false) + Logger.data.info("Favorited a node") } } else { - Image(systemName: "lock.open.fill") - .foregroundColor(.yellow) - } - Text(user.longName ?? "Unknown".localized) - .font(.headline) - .allowsTightening(true) - Spacer() - if user.userNode?.favorite ?? false { - Image(systemName: "star.fill") - .foregroundColor(.yellow) - } - if user.messageList.count > 0 { - if lastMessageDay == currentDay { - Text(lastMessageTime, style: .time ) - .font(.footnote) - .foregroundColor(.secondary) - } else if lastMessageDay == (currentDay - 1) { - Text("Yesterday") - .font(.footnote) - .foregroundColor(.secondary) - } else if lastMessageDay < (currentDay - 1) && lastMessageDay > (currentDay - 5) { - Text(lastMessageTime.formattedDate(format: dateFormatString)) - .font(.footnote) - .foregroundColor(.secondary) - } else if lastMessageDay < (currentDay - 1800) { - Text(lastMessageTime.formattedDate(format: dateFormatString)) - .font(.footnote) - .foregroundColor(.secondary) + let success = bleManager.removeFavoriteNode(node: user.userNode!, connectedNodeNum: Int64(node!.num)) + if success { + user.userNode?.favorite = !(user.userNode?.favorite ?? false) + Logger.data.info("Unfavorited a node") } } - } - - if user.messageList.count > 0 { - HStack(alignment: .top) { - Text("\(mostRecent != nil ? mostRecent!.messagePayload! : " ")") - .font(.footnote) - .foregroundColor(.secondary) + context.refresh(user, mergeChanges: true) + do { + try context.save() + } catch { + context.rollback() + Logger.data.error("Save Node Favorite Error") } - } - } - } - .frame(height: 62) - .contextMenu { - Button { - - if node != nil && !(user.userNode?.favorite ?? false) { - let success = bleManager.setFavoriteNode(node: user.userNode!, connectedNodeNum: Int64(node!.num)) - if success { - user.userNode?.favorite = !(user.userNode?.favorite ?? true) - Logger.data.info("Favorited a node") - } - } else { - let success = bleManager.removeFavoriteNode(node: user.userNode!, connectedNodeNum: Int64(node!.num)) - if success { - user.userNode?.favorite = !(user.userNode?.favorite ?? true) - Logger.data.info("Un Favorited a node") - } - } - context.refresh(user, mergeChanges: true) - do { - try context.save() - } catch { - context.rollback() - Logger.data.error("Save Node Favorite Error") - } - } label: { - Label((user.userNode?.favorite ?? false) ? "Un-Favorite" : "Favorite", systemImage: (user.userNode?.favorite ?? false) ? "star.slash.fill" : "star.fill") - } - Button { - user.mute = !user.mute - do { - try context.save() - } catch { - context.rollback() - Logger.data.error("Save User Mute Error") - } - } label: { - Label(user.mute ? "Show Alerts" : "Hide Alerts", systemImage: user.mute ? "bell" : "bell.slash") - } - if user.messageList.count > 0 { - Button(role: .destructive) { - isPresentingDeleteUserMessagesConfirm = true - userSelection = user } label: { - Label("Delete Messages", systemImage: "trash") + Label((user.userNode?.favorite ?? false) ? "Un-Favorite" : "Favorite", systemImage: (user.userNode?.favorite ?? false) ? "star.slash.fill" : "star.fill") + } + Button { + user.mute = !user.mute + do { + try context.save() + } catch { + context.rollback() + Logger.data.error("Save User Mute Error") + } + } label: { + Label(user.mute ? "Show Alerts" : "Hide Alerts", systemImage: user.mute ? "bell" : "bell.slash") + } + if user.messageList.count > 0 { + Button(role: .destructive) { + isPresentingDeleteUserMessagesConfirm = true + userSelection = user + } label: { + Label("Delete Messages", systemImage: "trash") + } } } - } - .confirmationDialog( - "This conversation will be deleted.", - isPresented: $isPresentingDeleteUserMessagesConfirm, - titleVisibility: .visible - ) { - Button(role: .destructive) { - deleteUserMessages(user: userSelection!, context: context) - context.refresh(node!.user!, mergeChanges: true) - } label: { - Text("Delete") + .confirmationDialog( + "This conversation will be deleted.", + isPresented: $isPresentingDeleteUserMessagesConfirm, + titleVisibility: .visible + ) { + Button(role: .destructive) { + deleteUserMessages(user: userSelection!, context: context) + context.refresh(node!.user!, mergeChanges: true) + } label: { + Text("Delete") + } } } } + .listStyle(.plain) + .navigationTitle(String.localizedStringWithFormat("Contacts (%@)", String(users.count == 0 ? 0 : users.count - 1))) } - .listStyle(.plain) - .navigationTitle(String.localizedStringWithFormat("Contacts (%@)".localized, String(users.count == 0 ? 0 : users.count - 1))) .sheet(isPresented: $editingFilters) { NodeListFilter(filterTitle: "Contact Filters", viaLora: $viaLora, viaMqtt: $viaMqtt, isOnline: $isOnline, isPkiEncrypted: $isPkiEncrypted, isFavorite: $isFavorite, isIgnored: $isIgnored, isEnvironment: $isEnvironment, distanceFilter: $distanceFilter, maximumDistance: $maxDistance, hopsAway: $hopsAway, roleFilter: $roleFilter, deviceRoles: $deviceRoles) } .sheet(isPresented: $showingHelp) { DirectMessagesHelp() } - .onChange(of: searchText) { - Task { - await searchUserList() - } - } - .onChange(of: viaLora) { - if !viaLora && !viaMqtt { - viaMqtt = true - } - Task { - await searchUserList() - } - } - .onChange(of: viaMqtt) { - if !viaLora && !viaMqtt { - viaLora = true - } - Task { - await searchUserList() - } - } - .onChange(of: [deviceRoles]) { - Task { - await searchUserList() - } - } - .onChange(of: hopsAway) { - Task { - await searchUserList() - } - } - .onChange(of: [boolFilters]) { - Task { - await searchUserList() - } - } - .onChange(of: maxDistance) { - Task { - await searchUserList() - } - } - .onChange(of: isPkiEncrypted) { - Task { - await searchUserList() - } - } - .onFirstAppear { - Task { - await searchUserList() - } - } .safeAreaInset(edge: .bottom, alignment: .leading) { HStack { Button(action: { @@ -281,36 +234,61 @@ struct UserList: View { .padding(5) } .padding(.bottom, 5) - .padding(.bottom, 5) - .searchable(text: $searchText, placement: users.count > 10 ? .navigationBarDrawer(displayMode: .always) : .automatic, prompt: "Find a contact") + .searchable(text: $searchText, placement: .navigationBarDrawer(displayMode: .always), prompt: "Find a contact") .disableAutocorrection(true) .scrollDismissesKeyboard(.immediately) } } - private func searchUserList() async { +} - /// Case Insensitive Search Text Predicates - let searchPredicates = ["userId", "numString", "hwModel", "hwDisplayName", "longName", "shortName"].map { property in - return NSPredicate(format: "%K CONTAINS[c] %@", property, searchText) - } - /// Create a compound predicate using each text search preicate as an OR - let textSearchPredicate = NSCompoundPredicate(type: .or, subpredicates: searchPredicates) - /// Create an array of predicates to hold our AND predicates +struct FilteredUserList: View { + @FetchRequest var fetchRequest: FetchedResults + let content: (FetchedResults) -> Content + + var body: some View { + content(fetchRequest) + } + + init( + searchText: String, + viaLora: Bool, + viaMqtt: Bool, + isOnline: Bool, + isPkiEncrypted: Bool, + isFavorite: Bool, + isIgnored: Bool, + isUnmessagable: Bool, + isEnvironment: Bool, + distanceFilter: Bool, + maxDistance: Double, + hopsAway: Double, + roleFilter: Bool, + deviceRoles: Set, + userSelection: Binding, + @ViewBuilder content: @escaping (FetchedResults) -> Content + ) { + self.content = content + // Build predicates based on filter variables var predicates: [NSPredicate] = [] - /// Mqtt and lora + // Search text predicates + if !searchText.isEmpty { + let searchPredicates = ["userId", "numString", "hwModel", "hwDisplayName", "longName", "shortName"].map { property in + return NSPredicate(format: "%K CONTAINS[c] %@", property, searchText) + } + let textSearchPredicate = NSCompoundPredicate(type: .or, subpredicates: searchPredicates) + predicates.append(textSearchPredicate) + } + // Mqtt and lora if !(viaLora && viaMqtt) { if viaLora { let loraPredicate = NSPredicate(format: "userNode.viaMqtt == NO") predicates.append(loraPredicate) } else { - let mqttPredicate = NSPredicate(format: "userNode.viaMqtt == YES") + let mqttPredicate = NSPredicate(format: "userNode.viaMqtt == YES AND userNode.hopsAway == 0") predicates.append(mqttPredicate) } - } else { - let mqttPredicate = NSPredicate(format: "NOT (userNode.viaMqtt == YES)") - predicates.append(mqttPredicate) } - /// Roles + // Roles if roleFilter && deviceRoles.count > 0 { var rolesArray: [NSPredicate] = [] for dr in deviceRoles { @@ -320,7 +298,7 @@ struct UserList: View { let compoundPredicate = NSCompoundPredicate(type: .or, subpredicates: rolesArray) predicates.append(compoundPredicate) } - /// Hops Away + // Hops Away if hopsAway == 0.0 { let hopsAwayPredicate = NSPredicate(format: "userNode.hopsAway == %i", Int32(hopsAway)) predicates.append(hopsAwayPredicate) @@ -328,32 +306,29 @@ struct UserList: View { let hopsAwayPredicate = NSPredicate(format: "userNode.hopsAway > 0 AND userNode.hopsAway <= %i", Int32(hopsAway)) predicates.append(hopsAwayPredicate) } - /// Online + // Online if isOnline { let isOnlinePredicate = NSPredicate(format: "userNode.lastHeard >= %@", Calendar.current.date(byAdding: .minute, value: -120, to: Date())! as NSDate) predicates.append(isOnlinePredicate) } - /// Encrypted + // Encrypted if isPkiEncrypted { let isPkiEncryptedPredicate = NSPredicate(format: "pkiEncrypted == YES") predicates.append(isPkiEncryptedPredicate) } - /// Favorites + // Favorites if isFavorite { let isFavoritePredicate = NSPredicate(format: "userNode.favorite == YES") predicates.append(isFavoritePredicate) } - /// Ignored + // Always apply these base filters let isIgnoredPredicate = NSPredicate(format: "userNode.ignored == NO") predicates.append(isIgnoredPredicate) - /// Unmessagable let isUnmessagablePredicate = NSPredicate(format: "unmessagable == NO") predicates.append(isUnmessagablePredicate) - - /// Distance + // Distance if distanceFilter { 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 @@ -368,11 +343,19 @@ struct UserList: View { predicates.append(distancePredicate) } } - if !searchText.isEmpty { - let filterPredicates = NSCompoundPredicate(type: .and, subpredicates: predicates) - users.nsPredicate = NSCompoundPredicate(type: .and, subpredicates: [textSearchPredicate, filterPredicates]) - } else { - users.nsPredicate = NSCompoundPredicate(type: .and, subpredicates: predicates) - } + // Combine all predicates + let finalPredicate = predicates.isEmpty ? NSPredicate(value: true) : NSCompoundPredicate(type: .and, subpredicates: predicates) + // Initialize the fetch request with the combined predicate + _fetchRequest = FetchRequest( + sortDescriptors: [ + NSSortDescriptor(key: "lastMessage", ascending: false), + NSSortDescriptor(key: "userNode.favorite", ascending: false), + NSSortDescriptor(key: "pkiEncrypted", ascending: false), + NSSortDescriptor(key: "userNode.lastHeard", ascending: false), + NSSortDescriptor(key: "longName", ascending: true) + ], + predicate: finalPredicate, + animation: .spring + ) } } From 219a85900a3f3c55e5dee1c32750d38b7357c17a Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Tue, 10 Jun 2025 17:10:03 -0700 Subject: [PATCH 088/213] Unredact client notification text in the logs --- Meshtastic/Helpers/BLEManager.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Meshtastic/Helpers/BLEManager.swift b/Meshtastic/Helpers/BLEManager.swift index 04e32452..a4a5389c 100644 --- a/Meshtastic/Helpers/BLEManager.swift +++ b/Meshtastic/Helpers/BLEManager.swift @@ -667,7 +667,7 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate ) ] manager.schedule() - Logger.data.error("⚠️ Client Notification \((try? decodedInfo.clientNotification.jsonString()) ?? "JSON Decode Failure")") + Logger.data.error("⚠️ Client Notification \((try? decodedInfo.clientNotification.jsonString()) ?? "JSON Decode Failure", privacy: .public)") } switch decodedInfo.packet.decoded.portnum { From 9e1766b90a1075e5c6c526673a3e5875bba6c0b0 Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Tue, 10 Jun 2025 17:54:19 -0700 Subject: [PATCH 089/213] Switch minimum firmware version back --- Meshtastic/Helpers/BLEManager.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Meshtastic/Helpers/BLEManager.swift b/Meshtastic/Helpers/BLEManager.swift index a4a5389c..05e38338 100644 --- a/Meshtastic/Helpers/BLEManager.swift +++ b/Meshtastic/Helpers/BLEManager.swift @@ -27,7 +27,7 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate @Published var automaticallyReconnect: Bool = true @Published var mqttProxyConnected: Bool = false @Published var mqttError: String = "" - public var minimumVersion = "2.5.14" + public var minimumVersion = "2.3.15" public var connectedVersion: String public var isConnecting: Bool = false public var isConnected: Bool = false From 269f784b2598453fd996eda0e118fd97f2e980b2 Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Tue, 10 Jun 2025 20:23:48 -0700 Subject: [PATCH 090/213] Show client notification message in the logs not the JSON --- Meshtastic/Helpers/BLEManager.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Meshtastic/Helpers/BLEManager.swift b/Meshtastic/Helpers/BLEManager.swift index 05e38338..ff822e90 100644 --- a/Meshtastic/Helpers/BLEManager.swift +++ b/Meshtastic/Helpers/BLEManager.swift @@ -659,7 +659,7 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate manager.notifications = [ Notification( id: UUID().uuidString, - title: "Firmware Notification", + title: "Firmware Notification".localized, subtitle: "\(decodedInfo.clientNotification.level)".capitalized, content: decodedInfo.clientNotification.message, target: "settings", @@ -667,7 +667,7 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate ) ] manager.schedule() - Logger.data.error("⚠️ Client Notification \((try? decodedInfo.clientNotification.jsonString()) ?? "JSON Decode Failure", privacy: .public)") + Logger.data.error("⚠️ Client Notification: \(decodedInfo.clientNotification.message, privacy: .public)") } switch decodedInfo.packet.decoded.portnum { From af98e807759c9094a4c946ae0e7d4c218ce95700 Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Tue, 10 Jun 2025 20:38:37 -0700 Subject: [PATCH 091/213] remove force unwrap from users on fetched nodes --- Meshtastic/Persistence/UpdateCoreData.swift | 22 ++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/Meshtastic/Persistence/UpdateCoreData.swift b/Meshtastic/Persistence/UpdateCoreData.swift index c53b2f73..22f76f04 100644 --- a/Meshtastic/Persistence/UpdateCoreData.swift +++ b/Meshtastic/Persistence/UpdateCoreData.swift @@ -284,13 +284,13 @@ func upsertNodeInfoPacket (packet: MeshPacket, context: NSManagedObjectContext) } if nodeInfoMessage.hasUser { /// Seeing Some crashes here ? - fetchedNode[0].user!.userId = nodeInfoMessage.user.id - fetchedNode[0].user!.num = Int64(nodeInfoMessage.num) - fetchedNode[0].user!.longName = nodeInfoMessage.user.longName - fetchedNode[0].user!.shortName = nodeInfoMessage.user.shortName - fetchedNode[0].user!.role = Int32(nodeInfoMessage.user.role.rawValue) - fetchedNode[0].user!.hwModel = String(describing: nodeInfoMessage.user.hwModel).uppercased() - fetchedNode[0].user!.hwModelId = Int32(nodeInfoMessage.user.hwModel.rawValue) + fetchedNode[0].user?.userId = nodeInfoMessage.user.id + fetchedNode[0].user?.num = Int64(nodeInfoMessage.num) + fetchedNode[0].user?.longName = nodeInfoMessage.user.longName + fetchedNode[0].user?.shortName = nodeInfoMessage.user.shortName + fetchedNode[0].user?.role = Int32(nodeInfoMessage.user.role.rawValue) + fetchedNode[0].user?.hwModel = String(describing: nodeInfoMessage.user.hwModel).uppercased() + fetchedNode[0].user?.hwModelId = Int32(nodeInfoMessage.user.hwModel.rawValue) /// For nodes that have the optional isUnmessagable boolean use that, otherwise excluded roles that are unmessagable by default if nodeInfoMessage.user.hasIsUnmessagable { fetchedNode[0].user!.unmessagable = nodeInfoMessage.user.isUnmessagable @@ -304,13 +304,13 @@ func upsertNodeInfoPacket (packet: MeshPacket, context: NSManagedObjectContext) } } if !nodeInfoMessage.user.publicKey.isEmpty { - fetchedNode[0].user!.pkiEncrypted = true - fetchedNode[0].user!.publicKey = nodeInfoMessage.user.publicKey + fetchedNode[0].user?.pkiEncrypted = true + fetchedNode[0].user?.publicKey = nodeInfoMessage.user.publicKey } Task { Api().loadDeviceHardwareData { (hw) in let dh = hw.first(where: { $0.hwModel == fetchedNode[0].user?.hwModelId ?? 0 }) - fetchedNode[0].user!.hwDisplayName = dh?.displayName + fetchedNode[0].user?.hwDisplayName = dh?.displayName } } } @@ -384,7 +384,7 @@ func upsertPositionPacket (packet: MeshPacket, context: NSManagedObjectContext) } else { position.time = Date(timeIntervalSince1970: TimeInterval(Int64(positionMessage.time))) } - guard let mutablePositions = fetchedNode[0].positions!.mutableCopy() as? NSMutableOrderedSet else { + guard let mutablePositions = fetchedNode[0].positions?.mutableCopy() as? NSMutableOrderedSet else { return } /// Don't save nearly the same position over and over. If the next position is less than 10 meters from the new position, delete the previous position and save the new one. From 0cdf1aeb1f583a6fab3cf605ae83d2a61483224e Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Tue, 10 Jun 2025 20:41:08 -0700 Subject: [PATCH 092/213] Missed a spot --- Meshtastic/Persistence/UpdateCoreData.swift | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/Meshtastic/Persistence/UpdateCoreData.swift b/Meshtastic/Persistence/UpdateCoreData.swift index 22f76f04..a286b08e 100644 --- a/Meshtastic/Persistence/UpdateCoreData.swift +++ b/Meshtastic/Persistence/UpdateCoreData.swift @@ -283,7 +283,6 @@ func upsertNodeInfoPacket (packet: MeshPacket, context: NSManagedObjectContext) fetchedNode[0].telemetries? = NSOrderedSet(array: newTelemetries) } if nodeInfoMessage.hasUser { - /// Seeing Some crashes here ? fetchedNode[0].user?.userId = nodeInfoMessage.user.id fetchedNode[0].user?.num = Int64(nodeInfoMessage.num) fetchedNode[0].user?.longName = nodeInfoMessage.user.longName @@ -293,7 +292,7 @@ func upsertNodeInfoPacket (packet: MeshPacket, context: NSManagedObjectContext) fetchedNode[0].user?.hwModelId = Int32(nodeInfoMessage.user.hwModel.rawValue) /// For nodes that have the optional isUnmessagable boolean use that, otherwise excluded roles that are unmessagable by default if nodeInfoMessage.user.hasIsUnmessagable { - fetchedNode[0].user!.unmessagable = nodeInfoMessage.user.isUnmessagable + fetchedNode[0].user?.unmessagable = nodeInfoMessage.user.isUnmessagable } else { let roles = [-1, 2, 4, 5, 6, 7, 10, 11] let containsRole = roles.contains(Int(fetchedNode[0].user?.role ?? -1)) From 7a5330dd4b997464cdc4c32196b50991b5f06d3d Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Tue, 10 Jun 2025 22:47:18 -0700 Subject: [PATCH 093/213] Slight updates to filter and title --- Meshtastic/Views/Messages/UserList.swift | 24 +++++++++++++++--------- 1 file changed, 15 insertions(+), 9 deletions(-) diff --git a/Meshtastic/Views/Messages/UserList.swift b/Meshtastic/Views/Messages/UserList.swift index 45f922e9..b3e6affd 100644 --- a/Meshtastic/Views/Messages/UserList.swift +++ b/Meshtastic/Views/Messages/UserList.swift @@ -196,7 +196,7 @@ struct UserList: View { } } .listStyle(.plain) - .navigationTitle(String.localizedStringWithFormat("Contacts (%@)", String(users.count == 0 ? 0 : users.count - 1))) + .navigationTitle(String.localizedStringWithFormat("Contacts (%@)", String(users.count))) } .sheet(isPresented: $editingFilters) { NodeListFilter(filterTitle: "Contact Filters", viaLora: $viaLora, viaMqtt: $viaMqtt, isOnline: $isOnline, isPkiEncrypted: $isPkiEncrypted, isFavorite: $isFavorite, isIgnored: $isIgnored, isEnvironment: $isEnvironment, distanceFilter: $distanceFilter, maximumDistance: $maxDistance, hopsAway: $hopsAway, roleFilter: $roleFilter, deviceRoles: $deviceRoles) @@ -244,11 +244,11 @@ struct UserList: View { struct FilteredUserList: View { @FetchRequest var fetchRequest: FetchedResults let content: (FetchedResults) -> Content - + var body: some View { content(fetchRequest) } - + init( searchText: String, viaLora: Bool, @@ -299,7 +299,7 @@ struct FilteredUserList: View { predicates.append(compoundPredicate) } // Hops Away - if hopsAway == 0.0 { + if hopsAway == 0 { let hopsAwayPredicate = NSPredicate(format: "userNode.hopsAway == %i", Int32(hopsAway)) predicates.append(hopsAwayPredicate) } else if hopsAway > -1.0 { @@ -321,11 +321,14 @@ struct FilteredUserList: View { let isFavoritePredicate = NSPredicate(format: "userNode.favorite == YES") predicates.append(isFavoritePredicate) } - // Always apply these base filters - let isIgnoredPredicate = NSPredicate(format: "userNode.ignored == NO") - predicates.append(isIgnoredPredicate) - let isUnmessagablePredicate = NSPredicate(format: "unmessagable == NO") - predicates.append(isUnmessagablePredicate) + // Ignored + if isIgnored { + let isIgnoredPredicate = NSPredicate(format: "userNode.ignored == YES") + predicates.append(isIgnoredPredicate) + } else if !isIgnored { + let isIgnoredPredicate = NSPredicate(format: "userNode.ignored == NO") + predicates.append(isIgnoredPredicate) + } // Distance if distanceFilter { let pointOfInterest = LocationsHandler.currentLocation @@ -343,6 +346,9 @@ struct FilteredUserList: View { predicates.append(distancePredicate) } } + // Always apply unmessagable filter + let isUnmessagablePredicate = NSPredicate(format: "unmessagable == NO") + predicates.append(isUnmessagablePredicate) // Combine all predicates let finalPredicate = predicates.isEmpty ? NSPredicate(value: true) : NSCompoundPredicate(type: .and, subpredicates: predicates) // Initialize the fetch request with the combined predicate From eda9bdf8adbe62308b82c46f69be3151b164778c Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Tue, 10 Jun 2025 22:57:00 -0700 Subject: [PATCH 094/213] Update Meshtastic/Views/Messages/UserList.swift Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- Meshtastic/Views/Messages/UserList.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Meshtastic/Views/Messages/UserList.swift b/Meshtastic/Views/Messages/UserList.swift index b3e6affd..1077f911 100644 --- a/Meshtastic/Views/Messages/UserList.swift +++ b/Meshtastic/Views/Messages/UserList.swift @@ -284,7 +284,7 @@ struct FilteredUserList: View { let loraPredicate = NSPredicate(format: "userNode.viaMqtt == NO") predicates.append(loraPredicate) } else { - let mqttPredicate = NSPredicate(format: "userNode.viaMqtt == YES AND userNode.hopsAway == 0") + let mqttPredicate = NSPredicate(format: "userNode.viaMqtt == YES") predicates.append(mqttPredicate) } } From 7b8980f1edff4cbe7cba4811f96e2ea16a625027 Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Tue, 10 Jun 2025 23:00:26 -0700 Subject: [PATCH 095/213] Remove unmessagable filter as it is static for the contact list --- Meshtastic/Views/Messages/UserList.swift | 2 -- 1 file changed, 2 deletions(-) diff --git a/Meshtastic/Views/Messages/UserList.swift b/Meshtastic/Views/Messages/UserList.swift index b3e6affd..7cb9ca55 100644 --- a/Meshtastic/Views/Messages/UserList.swift +++ b/Meshtastic/Views/Messages/UserList.swift @@ -56,7 +56,6 @@ struct UserList: View { isPkiEncrypted: isPkiEncrypted, isFavorite: isFavorite, isIgnored: isIgnored, - isUnmessagable: false, isEnvironment: isEnvironment, distanceFilter: distanceFilter, maxDistance: maxDistance, @@ -257,7 +256,6 @@ struct FilteredUserList: View { isPkiEncrypted: Bool, isFavorite: Bool, isIgnored: Bool, - isUnmessagable: Bool, isEnvironment: Bool, distanceFilter: Bool, maxDistance: Double, From 084da97da62ee0fa83c605e462eb2050b17de004 Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Tue, 10 Jun 2025 23:12:57 -0700 Subject: [PATCH 096/213] Dont be dumb --- Meshtastic/Views/Messages/UserList.swift | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/Meshtastic/Views/Messages/UserList.swift b/Meshtastic/Views/Messages/UserList.swift index 09bc8045..37749998 100644 --- a/Meshtastic/Views/Messages/UserList.swift +++ b/Meshtastic/Views/Messages/UserList.swift @@ -344,9 +344,11 @@ struct FilteredUserList: View { predicates.append(distancePredicate) } } - // Always apply unmessagable filter + // Always apply unmessagable and connected node filters let isUnmessagablePredicate = NSPredicate(format: "unmessagable == NO") predicates.append(isUnmessagablePredicate) + let isConnectedNodePredicate = NSPredicate(format: "NOT (numString CONTAINS \(UserDefaults.preferredPeripheralNum))") + predicates.append(isConnectedNodePredicate) // Combine all predicates let finalPredicate = predicates.isEmpty ? NSPredicate(value: true) : NSCompoundPredicate(type: .and, subpredicates: predicates) // Initialize the fetch request with the combined predicate From 2e92b838c4bd76be1de01298f7f0a98b584181d0 Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Tue, 10 Jun 2025 23:16:19 -0700 Subject: [PATCH 097/213] Update Meshtastic/Views/Messages/UserList.swift Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- Meshtastic/Views/Messages/UserList.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Meshtastic/Views/Messages/UserList.swift b/Meshtastic/Views/Messages/UserList.swift index 37749998..079b9593 100644 --- a/Meshtastic/Views/Messages/UserList.swift +++ b/Meshtastic/Views/Messages/UserList.swift @@ -347,7 +347,7 @@ struct FilteredUserList: View { // Always apply unmessagable and connected node filters let isUnmessagablePredicate = NSPredicate(format: "unmessagable == NO") predicates.append(isUnmessagablePredicate) - let isConnectedNodePredicate = NSPredicate(format: "NOT (numString CONTAINS \(UserDefaults.preferredPeripheralNum))") + let isConnectedNodePredicate = NSPredicate(format: "NOT (numString CONTAINS %@)", UserDefaults.preferredPeripheralNum) predicates.append(isConnectedNodePredicate) // Combine all predicates let finalPredicate = predicates.isEmpty ? NSPredicate(value: true) : NSCompoundPredicate(type: .and, subpredicates: predicates) From 8152642c6ca7732250185f3f35946a86d21aaa06 Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Tue, 10 Jun 2025 23:31:51 -0700 Subject: [PATCH 098/213] Try and fix logo on ios 26 --- Meshtastic/Views/Helpers/MeshtasticLogo.swift | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/Meshtastic/Views/Helpers/MeshtasticLogo.swift b/Meshtastic/Views/Helpers/MeshtasticLogo.swift index 84040d92..627ffdca 100644 --- a/Meshtastic/Views/Helpers/MeshtasticLogo.swift +++ b/Meshtastic/Views/Helpers/MeshtasticLogo.swift @@ -19,19 +19,20 @@ struct MeshtasticLogo: View { .renderingMode(.template) .foregroundColor(.accentColor) .scaledToFit() + .offset(x: -15) } .padding(.bottom, 5) .padding(.top, 5) - .offset(x: -15) + #else VStack { Image(colorScheme == .dark ? "logo-white" : "logo-black") .resizable() .renderingMode(.template) .scaledToFit() + .offset(x: -15) } .padding(.bottom, 5) - .offset(x: -15) #endif } } From 0e3d99c4580bf69b6f5d7f91d86223286824ace4 Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Wed, 11 Jun 2025 06:49:09 -0700 Subject: [PATCH 099/213] Fix userlist connected node predicate --- Meshtastic/Views/Helpers/MeshtasticLogo.swift | 6 ------ Meshtastic/Views/Messages/UserList.swift | 2 +- 2 files changed, 1 insertion(+), 7 deletions(-) diff --git a/Meshtastic/Views/Helpers/MeshtasticLogo.swift b/Meshtastic/Views/Helpers/MeshtasticLogo.swift index 627ffdca..c6906f15 100644 --- a/Meshtastic/Views/Helpers/MeshtasticLogo.swift +++ b/Meshtastic/Views/Helpers/MeshtasticLogo.swift @@ -11,26 +11,20 @@ struct MeshtasticLogo: View { @Environment(\.colorScheme) var colorScheme var body: some View { - #if targetEnvironment(macCatalyst) VStack { Image("logo-white") .resizable() - .renderingMode(.template) .foregroundColor(.accentColor) .scaledToFit() - .offset(x: -15) } .padding(.bottom, 5) .padding(.top, 5) - #else VStack { Image(colorScheme == .dark ? "logo-white" : "logo-black") .resizable() - .renderingMode(.template) .scaledToFit() - .offset(x: -15) } .padding(.bottom, 5) #endif diff --git a/Meshtastic/Views/Messages/UserList.swift b/Meshtastic/Views/Messages/UserList.swift index 079b9593..72ba1206 100644 --- a/Meshtastic/Views/Messages/UserList.swift +++ b/Meshtastic/Views/Messages/UserList.swift @@ -347,7 +347,7 @@ struct FilteredUserList: View { // Always apply unmessagable and connected node filters let isUnmessagablePredicate = NSPredicate(format: "unmessagable == NO") predicates.append(isUnmessagablePredicate) - let isConnectedNodePredicate = NSPredicate(format: "NOT (numString CONTAINS %@)", UserDefaults.preferredPeripheralNum) + 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) From f1d69ac5bb88f9666d42f51ce978eb57d3a58548 Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Wed, 11 Jun 2025 09:25:22 -0700 Subject: [PATCH 100/213] Revert non working changes to firmware link for latest stable version --- Meshtastic/Views/Settings/Firmware.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Meshtastic/Views/Settings/Firmware.swift b/Meshtastic/Views/Settings/Firmware.swift index 0e21850a..2380b677 100644 --- a/Meshtastic/Views/Settings/Firmware.swift +++ b/Meshtastic/Views/Settings/Firmware.swift @@ -199,7 +199,7 @@ struct Firmware: View { latestStable = fw.releases.stable.first let archString = currentDevice?.architecture.rawValue ?? "" let ls = fw.releases.stable.first(where: { $0.zipURL.contains(archString) == true }) - latestStable = fw.releases.stable.first(where: { $0.zipURL.contains(archString) == true }) + latestStable = fw.releases.stable.first latestAlpha = fw.releases.alpha.first } } From 7466c648e6e91951116aa3d2217977fc9d396c72 Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Wed, 11 Jun 2025 09:55:29 -0700 Subject: [PATCH 101/213] Remove ignored filter from node filter. --- Meshtastic/Views/Messages/UserList.swift | 10 ++------ .../Views/Nodes/Helpers/NodeListFilter.swift | 25 ++++++++++--------- 2 files changed, 15 insertions(+), 20 deletions(-) diff --git a/Meshtastic/Views/Messages/UserList.swift b/Meshtastic/Views/Messages/UserList.swift index 72ba1206..41642582 100644 --- a/Meshtastic/Views/Messages/UserList.swift +++ b/Meshtastic/Views/Messages/UserList.swift @@ -319,14 +319,6 @@ struct FilteredUserList: View { let isFavoritePredicate = NSPredicate(format: "userNode.favorite == YES") predicates.append(isFavoritePredicate) } - // Ignored - if isIgnored { - let isIgnoredPredicate = NSPredicate(format: "userNode.ignored == YES") - predicates.append(isIgnoredPredicate) - } else if !isIgnored { - let isIgnoredPredicate = NSPredicate(format: "userNode.ignored == NO") - predicates.append(isIgnoredPredicate) - } // Distance if distanceFilter { let pointOfInterest = LocationsHandler.currentLocation @@ -347,6 +339,8 @@ struct FilteredUserList: View { // Always apply unmessagable and connected node filters let isUnmessagablePredicate = NSPredicate(format: "unmessagable == NO") predicates.append(isUnmessagablePredicate) + let isIgnoredPredicate = NSPredicate(format: "userNode.ignored == NO") + predicates.append(isIgnoredPredicate) let isConnectedNodePredicate = NSPredicate(format: "NOT (numString CONTAINS %@)", String(UserDefaults.preferredPeripheralNum)) predicates.append(isConnectedNodePredicate) // Combine all predicates diff --git a/Meshtastic/Views/Nodes/Helpers/NodeListFilter.swift b/Meshtastic/Views/Nodes/Helpers/NodeListFilter.swift index 063e073a..1a02cfd7 100644 --- a/Meshtastic/Views/Nodes/Helpers/NodeListFilter.swift +++ b/Meshtastic/Views/Nodes/Helpers/NodeListFilter.swift @@ -91,20 +91,21 @@ struct NodeListFilter: View { } .toggleStyle(SwitchToggleStyle(tint: .accentColor)) .listRowSeparator(.visible) - Toggle(isOn: $isIgnored) { - - Label { - Text("Ignored") - } icon: { - - Image(systemName: "minus.circle.fill") - .symbolRenderingMode(.multicolor) - } - } - .toggleStyle(SwitchToggleStyle(tint: .accentColor)) - .listRowSeparator(.visible) if filterTitle == "Node Filters" { + Toggle(isOn: $isIgnored) { + + Label { + Text("Ignored") + } icon: { + + Image(systemName: "minus.circle.fill") + .symbolRenderingMode(.multicolor) + } + } + .toggleStyle(SwitchToggleStyle(tint: .accentColor)) + .listRowSeparator(.visible) + Toggle(isOn: $isEnvironment) { Label { Text("Environment") From 5da837701bf842268501fa31632d651f8d24817d Mon Sep 17 00:00:00 2001 From: Benjamin Faershtein <119711889+RCGV1@users.noreply.github.com> Date: Wed, 11 Jun 2025 10:37:50 -0700 Subject: [PATCH 102/213] Added copying public key button --- .../Views/Nodes/Helpers/NodeDetail.swift | 25 +++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/Meshtastic/Views/Nodes/Helpers/NodeDetail.swift b/Meshtastic/Views/Nodes/Helpers/NodeDetail.swift index b2c97074..dfd54b02 100644 --- a/Meshtastic/Views/Nodes/Helpers/NodeDetail.swift +++ b/Meshtastic/Views/Nodes/Helpers/NodeDetail.swift @@ -122,6 +122,31 @@ struct NodeDetail: View { .textSelection(.enabled) } .accessibilityElement(children: .combine) + + if node.user?.keyMatch ?? false { + if let publicKey = node.user?.publicKey { + HStack { + Label { + Text("Public Key") + } icon: { + Image(systemName: "lock.fill") + .foregroundColor(.green) + } + Spacer() + Button(action: { + context.perform{ + UIPasteboard.general.string = publicKey.base64EncodedString() + } + }) { + HStack { + Image(systemName: "key.horizontal.fill") + Text("Copy") + } + } + } + .accessibilityElement(children: .combine) + } + } if let metadata = node.metadata { HStack { From c16ef7f118cf838295503c30feed61767023371c Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Wed, 11 Jun 2025 16:54:40 -0700 Subject: [PATCH 103/213] TCP Connection warning, accent tint color for udp toggle --- Localizable.xcstrings | 4 ++++ Meshtastic.xcodeproj/project.pbxproj | 12 ++++++------ Meshtastic/Views/Settings/Config/NetworkConfig.swift | 7 ++++--- 3 files changed, 14 insertions(+), 9 deletions(-) diff --git a/Localizable.xcstrings b/Localizable.xcstrings index eec7ee0d..ccf11c60 100644 --- a/Localizable.xcstrings +++ b/Localizable.xcstrings @@ -10748,6 +10748,7 @@ } }, "Enabling WiFi will disable the bluetooth connection to the app." : { + "extractionState" : "stale", "localizations" : { "it" : { "stringUnit" : { @@ -10774,6 +10775,9 @@ } } } + }, + "Enabling WiFi will disable the bluetooth connection to the app. TCP node connections are not available on apple devices." : { + }, "Encoder Press Event" : { "localizations" : { diff --git a/Meshtastic.xcodeproj/project.pbxproj b/Meshtastic.xcodeproj/project.pbxproj index ec788769..6b37e6a6 100644 --- a/Meshtastic.xcodeproj/project.pbxproj +++ b/Meshtastic.xcodeproj/project.pbxproj @@ -767,6 +767,7 @@ children = ( DDD5BB0E2C285F92007E03CA /* Logs */, DD93800C2BA74CE3008BEC06 /* Channels */, + DD61937A2863876A00E59241 /* Config */, DD97E96728EFE9A00056DDA4 /* About.swift */, DDD5BB152C28B1E4007E03CA /* AppData.swift */, DDD5BB082C285DDC007E03CA /* AppLog.swift */, @@ -780,7 +781,6 @@ DD3501882852FC3B000FC853 /* Settings.swift */, DD3CC6B428E33FD100FA9159 /* ShareChannels.swift */, DDCE4E2B2869F92900BE9F8F /* UserConfig.swift */, - DD61937A2863876A00E59241 /* Config */, DD1B8F3F2B35E2F10022AABC /* GPSStatus.swift */, ); path = Settings; @@ -801,6 +801,7 @@ DD61937A2863876A00E59241 /* Config */ = { isa = PBXGroup; children = ( + DD61937B2863877A00E59241 /* Module */, D93068DC2B81CA820066FBC8 /* ConfigHeader.swift */, D93069072B81DF040066FBC8 /* SaveConfigButton.swift */, DDB6ABD528AE742000384BA1 /* BluetoothConfig.swift */, @@ -811,7 +812,6 @@ DD2553582855B52700E55709 /* PositionConfig.swift */, D93068DA2B81C85E0066FBC8 /* PowerConfig.swift */, DD1BD0F22C63C65E008C0C70 /* SecurityConfig.swift */, - DD61937B2863877A00E59241 /* Module */, ); path = Config; sourceTree = ""; @@ -1807,7 +1807,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 2.6.4; + MARKETING_VERSION = 2.6.5; PRODUCT_BUNDLE_IDENTIFIER = gvh.MeshtasticClient; PRODUCT_NAME = "$(TARGET_NAME)"; SUPPORTS_MACCATALYST = YES; @@ -1840,7 +1840,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 2.6.4; + MARKETING_VERSION = 2.6.5; PRODUCT_BUNDLE_IDENTIFIER = gvh.MeshtasticClient; PRODUCT_NAME = "$(TARGET_NAME)"; SUPPORTS_MACCATALYST = YES; @@ -1871,7 +1871,7 @@ "@executable_path/Frameworks", "@executable_path/../../Frameworks", ); - MARKETING_VERSION = 2.6.4; + MARKETING_VERSION = 2.6.5; PRODUCT_BUNDLE_IDENTIFIER = gvh.MeshtasticClient.Widgets; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; @@ -1903,7 +1903,7 @@ "@executable_path/Frameworks", "@executable_path/../../Frameworks", ); - MARKETING_VERSION = 2.6.4; + MARKETING_VERSION = 2.6.5; PRODUCT_BUNDLE_IDENTIFIER = gvh.MeshtasticClient.Widgets; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; diff --git a/Meshtastic/Views/Settings/Config/NetworkConfig.swift b/Meshtastic/Views/Settings/Config/NetworkConfig.swift index c4dcb8f2..57270f65 100644 --- a/Meshtastic/Views/Settings/Config/NetworkConfig.swift +++ b/Meshtastic/Views/Settings/Config/NetworkConfig.swift @@ -37,7 +37,7 @@ struct NetworkConfig: View { Toggle(isOn: $wifiEnabled) { Label("Enabled", systemImage: "wifi") - Text("Enabling WiFi will disable the bluetooth connection to the app.") + Text("Enabling WiFi will disable the bluetooth connection to the app. TCP node connections are not available on Apple devices.") } .toggleStyle(SwitchToggleStyle(tint: .accentColor)) @@ -83,9 +83,9 @@ struct NetworkConfig: View { Section(header: Text("Ethernet Options")) { Toggle(isOn: $ethEnabled) { Label("Enabled", systemImage: "network") - Text("Enabling Ethernet will disable the bluetooth connection to the app.") + Text("Enabling Ethernet will disable the bluetooth connection to the app. TCP node connections are not available on Apple devices.") } - .toggleStyle(SwitchToggleStyle(tint: .accentColor)) + .tint(.accentColor) } } @@ -95,6 +95,7 @@ struct NetworkConfig: View { Label("Enabled", systemImage: "point.3.connected.trianglepath.dotted") Text("Enable broadcasting packets via UDP over the local network.") } + .tint(.accentColor) } } } From 25ca292c55e9c9bb29dd047785a7d0383ec6f3ab Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Thu, 12 Jun 2025 08:43:07 -0700 Subject: [PATCH 104/213] Add option to reset keys and ble bonds to factory reset --- Localizable.xcstrings | 123 ++---------------- Meshtastic/Helpers/BLEManager.swift | 8 +- .../Views/Settings/Config/DeviceConfig.swift | 14 +- 3 files changed, 29 insertions(+), 116 deletions(-) diff --git a/Localizable.xcstrings b/Localizable.xcstrings index ccf11c60..9ea910d4 100644 --- a/Localizable.xcstrings +++ b/Localizable.xcstrings @@ -2809,34 +2809,6 @@ } } }, - "All device and app data will be deleted." : { - "localizations" : { - "it" : { - "stringUnit" : { - "state" : "translated", - "value" : "Tutti i dati del dispositivo e delle app verranno eliminati." - } - }, - "sr" : { - "stringUnit" : { - "state" : "translated", - "value" : "Сви подаци о уређају и апликацији ће бити избрисани." - } - }, - "zh-Hans" : { - "stringUnit" : { - "state" : "translated", - "value" : "所有设备以及 App 数据都会被删除。" - } - }, - "zh-Hant-TW" : { - "stringUnit" : { - "state" : "translated", - "value" : "全部的設備及App資料將會被刪除。" - } - } - } - }, "Allow incoming device control over the insecure legacy admin channel." : { "localizations" : { "de" : { @@ -8139,6 +8111,12 @@ } } } + }, + "Delete all config, keys and BLE bonds? " : { + + }, + "Delete all config? " : { + }, "Delete all device metrics?" : { "localizations" : { @@ -10719,64 +10697,10 @@ } } }, - "Enabling Ethernet will disable the bluetooth connection to the app." : { - "localizations" : { - "it" : { - "stringUnit" : { - "state" : "translated", - "value" : "Abilitando l'Ethernet si disabilita la connessione bluetooth all'applicazione." - } - }, - "sr" : { - "stringUnit" : { - "state" : "translated", - "value" : "Омогућавање етернета ће онемогућити блутут везу са апликацијом." - } - }, - "zh-Hans" : { - "stringUnit" : { - "state" : "translated", - "value" : "启用以太网将禁用应用程序的蓝牙连接。" - } - }, - "zh-Hant-TW" : { - "stringUnit" : { - "state" : "translated", - "value" : "啟用乙太網路後,將會停用與應用程式的藍牙連線。" - } - } - } + "Enabling Ethernet will disable the bluetooth connection to the app. TCP node connections are not available on Apple devices." : { + }, - "Enabling WiFi will disable the bluetooth connection to the app." : { - "extractionState" : "stale", - "localizations" : { - "it" : { - "stringUnit" : { - "state" : "translated", - "value" : "L'attivazione del WiFi disabilita la connessione bluetooth all'applicazione." - } - }, - "sr" : { - "stringUnit" : { - "state" : "translated", - "value" : "Омогућавање ВајФаја ће онемогућити блутут везу са апликацијом." - } - }, - "zh-Hans" : { - "stringUnit" : { - "state" : "translated", - "value" : "启用 WiFi 将禁用应用程序的蓝牙连接。" - } - }, - "zh-Hant-TW" : { - "stringUnit" : { - "state" : "translated", - "value" : "啟用 Wi-Fi 後,將會停用與應用程式的藍牙連線。" - } - } - } - }, - "Enabling WiFi will disable the bluetooth connection to the app. TCP node connections are not available on apple devices." : { + "Enabling WiFi will disable the bluetooth connection to the app. TCP node connections are not available on Apple devices." : { }, "Encoder Press Event" : { @@ -11677,33 +11601,8 @@ } } }, - "Factory reset your device and app? " : { - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Gerät und App auf Werkseinstellungen zurücksetzen?" - } - }, - "it" : { - "stringUnit" : { - "state" : "translated", - "value" : "Resettare il dispositivo e l'applicazione? " - } - }, - "sr" : { - "stringUnit" : { - "state" : "translated", - "value" : "Вратите уређај и апликацију на фабричка подешавања?" - } - }, - "zh-Hant-TW" : { - "stringUnit" : { - "state" : "translated", - "value" : "要將您的裝置與應用程式恢復原廠設定嗎?" - } - } - } + "Factory reset will delete device and app data." : { + }, "Failed to encode message content" : { "localizations" : { diff --git a/Meshtastic/Helpers/BLEManager.swift b/Meshtastic/Helpers/BLEManager.swift index ff822e90..c1dc7023 100644 --- a/Meshtastic/Helpers/BLEManager.swift +++ b/Meshtastic/Helpers/BLEManager.swift @@ -1532,9 +1532,13 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate return false } - public func sendFactoryReset(fromUser: UserEntity, toUser: UserEntity) -> Bool { + public func sendFactoryReset(fromUser: UserEntity, toUser: UserEntity, resetDevice: Bool = false) -> Bool { var adminPacket = AdminMessage() - adminPacket.factoryResetConfig = 5 + if resetDevice { + adminPacket.factoryResetDevice = 5 + } else { + adminPacket.factoryResetConfig = 5 + } if fromUser != toUser { adminPacket.sessionPasskey = toUser.userNode?.sessionPasskey ?? Data() } diff --git a/Meshtastic/Views/Settings/Config/DeviceConfig.swift b/Meshtastic/Views/Settings/Config/DeviceConfig.swift index cd812e5d..bb9b6916 100644 --- a/Meshtastic/Views/Settings/Config/DeviceConfig.swift +++ b/Meshtastic/Views/Settings/Config/DeviceConfig.swift @@ -204,11 +204,11 @@ struct DeviceConfig: View { .controlSize(.regular) .padding(.trailing) .confirmationDialog( - "All device and app data will be deleted.", + "Factory reset will delete device and app data.", isPresented: $isPresentingFactoryResetConfirm, titleVisibility: .visible ) { - Button("Factory reset your device and app? ", role: .destructive) { + Button("Delete all config? ", role: .destructive) { if bleManager.sendFactoryReset(fromUser: node!.user!, toUser: node!.user!) { DispatchQueue.main.asyncAfter(deadline: .now() + 1) { bleManager.disconnectPeripheral() @@ -218,6 +218,16 @@ struct DeviceConfig: View { Logger.mesh.error("Factory Reset Failed") } } + Button("Delete all config, keys and BLE bonds? ", role: .destructive) { + if bleManager.sendFactoryReset(fromUser: node!.user!, toUser: node!.user!, resetDevice: true) { + DispatchQueue.main.asyncAfter(deadline: .now() + 1) { + bleManager.disconnectPeripheral() + clearCoreDataDatabase(context: context, includeRoutes: false) + } + } else { + Logger.mesh.error("Factory Reset Failed") + } + } } } } From f3316518cc2a8c021e3c9664bb1b31f5589c245d Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Thu, 12 Jun 2025 09:44:31 -0700 Subject: [PATCH 105/213] Remove reply overlay until it can look decent --- Localizable.xcstrings | 1 + Meshtastic/Views/Messages/ChannelMessageList.swift | 11 +++++------ .../Messages/TextMessageField/TextMessageField.swift | 3 ++- Meshtastic/Views/Messages/UserMessageList.swift | 10 +++++----- 4 files changed, 13 insertions(+), 12 deletions(-) diff --git a/Localizable.xcstrings b/Localizable.xcstrings index 9ea910d4..7b68ae40 100644 --- a/Localizable.xcstrings +++ b/Localizable.xcstrings @@ -24354,6 +24354,7 @@ } }, "Replying to a message" : { + "extractionState" : "stale", "localizations" : { "zh-Hant-TW" : { "stringUnit" : { diff --git a/Meshtastic/Views/Messages/ChannelMessageList.swift b/Meshtastic/Views/Messages/ChannelMessageList.swift index 458d20ed..9dd3b5cf 100644 --- a/Meshtastic/Views/Messages/ChannelMessageList.swift +++ b/Meshtastic/Views/Messages/ChannelMessageList.swift @@ -136,12 +136,11 @@ struct ChannelMessageList: View { Spacer(minLength: 50) } } - - .overlay { - RoundedRectangle(cornerRadius: 10) - .stroke(.blue, lineWidth: 2) - .opacity(((messageToHighlight == message.messageId) || (replyMessageId == message.messageId)) ? 1 : 0) - } +// .overlay { +// RoundedRectangle(cornerRadius: 18) +// .stroke(.blue, lineWidth: 2) +// .opacity(((messageToHighlight == message.messageId) || (replyMessageId == message.messageId)) ? 1 : 0) +// } .padding([.leading, .trailing]) .frame(maxWidth: .infinity) .id(message.messageId) diff --git a/Meshtastic/Views/Messages/TextMessageField/TextMessageField.swift b/Meshtastic/Views/Messages/TextMessageField/TextMessageField.swift index d60a0381..945b41c0 100644 --- a/Meshtastic/Views/Messages/TextMessageField/TextMessageField.swift +++ b/Meshtastic/Views/Messages/TextMessageField/TextMessageField.swift @@ -39,8 +39,9 @@ struct TextMessageField: View { } label: { Image(systemName: "x.circle.fill") } - Text("Replying to a message") + Text("Reply") } + .padding(.top) } ZStack { diff --git a/Meshtastic/Views/Messages/UserMessageList.swift b/Meshtastic/Views/Messages/UserMessageList.swift index 2a4756fd..e84686d6 100644 --- a/Meshtastic/Views/Messages/UserMessageList.swift +++ b/Meshtastic/Views/Messages/UserMessageList.swift @@ -123,11 +123,11 @@ struct UserMessageList: View { Spacer(minLength: 50) } } - .overlay { - RoundedRectangle(cornerRadius: 10) - .stroke(.blue, lineWidth: 2) - .opacity(((messageToHighlight == message.messageId) || (replyMessageId == message.messageId)) ? 1 : 0) - } +// .overlay { +// RoundedRectangle(cornerRadius: 10) +// .stroke(.blue, lineWidth: 2) +// .opacity(((messageToHighlight == message.messageId) || (replyMessageId == message.messageId)) ? 1 : 0) +// } .padding([.leading, .trailing]) .frame(maxWidth: .infinity) .id(message.messageId) From f4ec895fb9502b6b9c794cec9357c66a148f86ba Mon Sep 17 00:00:00 2001 From: Benjamin Faershtein <119711889+RCGV1@users.noreply.github.com> Date: Thu, 12 Jun 2025 10:51:48 -0700 Subject: [PATCH 106/213] Seperated button --- Localizable.xcstrings | 3 +++ Meshtastic/Views/Nodes/Helpers/Map/WaypointForm.swift | 7 +++++-- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/Localizable.xcstrings b/Localizable.xcstrings index b5d0c59d..de82186b 100644 --- a/Localizable.xcstrings +++ b/Localizable.xcstrings @@ -33445,6 +33445,9 @@ } } } + }, + "Use my Location" : { + }, "Use Preset" : { "localizations" : { diff --git a/Meshtastic/Views/Nodes/Helpers/Map/WaypointForm.swift b/Meshtastic/Views/Nodes/Helpers/Map/WaypointForm.swift index 5279dfe2..5866e8ed 100644 --- a/Meshtastic/Views/Nodes/Helpers/Map/WaypointForm.swift +++ b/Meshtastic/Views/Nodes/Helpers/Map/WaypointForm.swift @@ -49,15 +49,18 @@ struct WaypointForm: View { .foregroundColor(.secondary) .font(.caption) + } Button { let currentLoc = LocationsHandler.currentLocation waypoint.coordinate.longitude = currentLoc.longitude waypoint.coordinate.latitude = currentLoc.latitude } label: { - Image(systemName: "location") + HStack { + Text("Use my Location") + Image(systemName: "location") + } } .accessibilityLabel("Set to current location") - } HStack { if waypoint.coordinate.latitude != 0 && waypoint.coordinate.longitude != 0 { DistanceText(meters: distance) From b6b95accadcb3086660b457873056efaf4276118 Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Thu, 12 Jun 2025 10:52:30 -0700 Subject: [PATCH 107/213] 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 6b37e6a6..14a1c9b1 100644 --- a/Meshtastic.xcodeproj/project.pbxproj +++ b/Meshtastic.xcodeproj/project.pbxproj @@ -1807,7 +1807,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 2.6.5; + MARKETING_VERSION = 2.6.6; PRODUCT_BUNDLE_IDENTIFIER = gvh.MeshtasticClient; PRODUCT_NAME = "$(TARGET_NAME)"; SUPPORTS_MACCATALYST = YES; @@ -1840,7 +1840,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 2.6.5; + MARKETING_VERSION = 2.6.6; PRODUCT_BUNDLE_IDENTIFIER = gvh.MeshtasticClient; PRODUCT_NAME = "$(TARGET_NAME)"; SUPPORTS_MACCATALYST = YES; @@ -1871,7 +1871,7 @@ "@executable_path/Frameworks", "@executable_path/../../Frameworks", ); - MARKETING_VERSION = 2.6.5; + MARKETING_VERSION = 2.6.6; PRODUCT_BUNDLE_IDENTIFIER = gvh.MeshtasticClient.Widgets; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; @@ -1903,7 +1903,7 @@ "@executable_path/Frameworks", "@executable_path/../../Frameworks", ); - MARKETING_VERSION = 2.6.5; + MARKETING_VERSION = 2.6.6; PRODUCT_BUNDLE_IDENTIFIER = gvh.MeshtasticClient.Widgets; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; From e17791b71d79709953d67314a147ee859f31fa1b Mon Sep 17 00:00:00 2001 From: Benjamin Faershtein <119711889+RCGV1@users.noreply.github.com> Date: Thu, 12 Jun 2025 12:06:23 -0700 Subject: [PATCH 108/213] First get config to subscribe then fetch nodes --- Meshtastic/Helpers/BLEManager.swift | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/Meshtastic/Helpers/BLEManager.swift b/Meshtastic/Helpers/BLEManager.swift index c1dc7023..008fe3e8 100644 --- a/Meshtastic/Helpers/BLEManager.swift +++ b/Meshtastic/Helpers/BLEManager.swift @@ -511,7 +511,10 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate Logger.mesh.info("🛎️ \(logString, privacy: .public)") // BLE Characteristics discovered, issue wantConfig var toRadio: ToRadio = ToRadio() - configNonce += 1 + configNonce = UInt32(69421) + if !isSubscribed { + configNonce = UInt32(69420) // Get config first + } toRadio.wantConfigID = configNonce guard let binaryData: Data = try? toRadio.serializedData() else { return @@ -982,7 +985,7 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate Logger.mesh.warning("🕸️ MESH PACKET received for Key Verification App UNHANDLED \((try? decodedInfo.packet.jsonString()) ?? "JSON Decode Failure", privacy: .public)") } - if decodedInfo.configCompleteID != 0 && decodedInfo.configCompleteID == configNonce { + if decodedInfo.configCompleteID != 0 && decodedInfo.configCompleteID == 69420 { invalidVersion = false lastConnectionError = "" isSubscribed = true @@ -1022,6 +1025,12 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate } catch { Logger.data.error("Failed to find a node info for the connected node \(error.localizedDescription, privacy: .public)") } + Logger.mesh.info("🤜 [BLE] Want Config Complete. ID:\(decodedInfo.configCompleteID, privacy: .public)") + sendWantConfig() + + } + if decodedInfo.configCompleteID != 0 && decodedInfo.configCompleteID == 69421 { + Logger.mesh.info("🤜 [BLE] Want Config DB Complete. ID:\(decodedInfo.configCompleteID, privacy: .public)") } // MARK: Share Location Position Update Timer From 57d6a8c721c655e5644bfcb9111a95f85cf53a76 Mon Sep 17 00:00:00 2001 From: Benjamin Faershtein <119711889+RCGV1@users.noreply.github.com> Date: Thu, 12 Jun 2025 14:12:00 -0700 Subject: [PATCH 109/213] Fixed messages not scrolling to bottom --- .../Views/Messages/ChannelMessageList.swift | 20 +++++++++---------- .../Views/Messages/UserMessageList.swift | 20 +++++++++---------- 2 files changed, 20 insertions(+), 20 deletions(-) diff --git a/Meshtastic/Views/Messages/ChannelMessageList.swift b/Meshtastic/Views/Messages/ChannelMessageList.swift index 9dd3b5cf..42bd5c12 100644 --- a/Meshtastic/Views/Messages/ChannelMessageList.swift +++ b/Meshtastic/Views/Messages/ChannelMessageList.swift @@ -180,24 +180,24 @@ struct ChannelMessageList: View { } .scrollDismissesKeyboard(.interactively) .onFirstAppear { - // Find first unread message - if let firstUnreadMessageId = channel.allPrivateMessages.first(where: { !$0.read })?.messageId { + if channel.unreadMessages == 0 { withAnimation { - scrollView.scrollTo(firstUnreadMessageId, anchor: .top) - showScrollToBottomButton = true + scrollView.scrollTo("bottomAnchor", anchor: .bottom) + hasReachedBottom = true } } else { - // If no unread messages, scroll to bottom - withAnimation { - scrollView.scrollTo(channel.allPrivateMessages.last?.messageId ?? 0, anchor: .bottom) - hasReachedBottom = true + if let firstUnreadMessageId = channel.allPrivateMessages.first(where: { !$0.read })?.messageId { + withAnimation { + scrollView.scrollTo(firstUnreadMessageId, anchor: .top) + showScrollToBottomButton = true + } } } gotFirstUnreadMessage = true } .onReceive(NotificationCenter.default.publisher(for: UIResponder.keyboardDidShowNotification)) { _ in withAnimation { - scrollView.scrollTo(channel.allPrivateMessages.last?.messageId ?? 0, anchor: .bottom) + scrollView.scrollTo("bottomAnchor", anchor: .bottom) hasReachedBottom = true showScrollToBottomButton = false } @@ -205,7 +205,7 @@ struct ChannelMessageList: View { .onChange(of: channel.allPrivateMessages) { if hasReachedBottom { withAnimation { - scrollView.scrollTo(channel.allPrivateMessages.last?.messageId ?? 0, anchor: .bottom) + scrollView.scrollTo("bottomAnchor", anchor: .bottom) } } else { showScrollToBottomButton = true diff --git a/Meshtastic/Views/Messages/UserMessageList.swift b/Meshtastic/Views/Messages/UserMessageList.swift index e84686d6..7b27b4f2 100644 --- a/Meshtastic/Views/Messages/UserMessageList.swift +++ b/Meshtastic/Views/Messages/UserMessageList.swift @@ -167,24 +167,24 @@ struct UserMessageList: View { } .scrollDismissesKeyboard(.interactively) .onFirstAppear { - // Find first unread message - if let firstUnreadMessageId = user.messageList.first(where: { !$0.read })?.messageId { + if user.unreadMessages == 0 { withAnimation { - scrollView.scrollTo(firstUnreadMessageId, anchor: .top) - showScrollToBottomButton = true + scrollView.scrollTo("bottomAnchor", anchor: .bottom) + hasReachedBottom = true } } else { - // If no unread messages, scroll to bottom - withAnimation { - scrollView.scrollTo(user.messageList.last?.messageId ?? 0, anchor: .bottom) - hasReachedBottom = true + if let firstUnreadMessageId = user.messageList.first(where: { !$0.read })?.messageId { + withAnimation { + scrollView.scrollTo(firstUnreadMessageId, anchor: .top) + showScrollToBottomButton = true + } } } gotFirstUnreadMessage = true } .onReceive(NotificationCenter.default.publisher(for: UIResponder.keyboardDidShowNotification)) { _ in withAnimation { - scrollView.scrollTo(user.messageList.last?.messageId ?? 0, anchor: .bottom) + scrollView.scrollTo("bottomAnchor", anchor: .bottom) hasReachedBottom = true showScrollToBottomButton = false } @@ -192,7 +192,7 @@ struct UserMessageList: View { .onChange(of: user.messageList) { if hasReachedBottom { withAnimation { - scrollView.scrollTo(user.messageList.last?.messageId ?? 0, anchor: .bottom) + scrollView.scrollTo("bottomAnchor", anchor: .bottom) } } else { showScrollToBottomButton = true From 2920a38312bbe3861f924c12a3f84436e2dfcf14 Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Fri, 13 Jun 2025 09:57:27 -0700 Subject: [PATCH 110/213] Translation updates --- Localizable.xcstrings | 45 ++------- Meshtastic/Helpers/MeshPackets.swift | 138 +++++++++++++------------- Meshtastic/Views/Settings/About.swift | 4 +- 3 files changed, 78 insertions(+), 109 deletions(-) diff --git a/Localizable.xcstrings b/Localizable.xcstrings index 7b68ae40..401017b9 100644 --- a/Localizable.xcstrings +++ b/Localizable.xcstrings @@ -13243,6 +13243,9 @@ } } } + }, + "GitHub Repository" : { + }, "Good" : { "localizations" : { @@ -13816,34 +13819,6 @@ } } }, - "Help with App Development" : { - "localizations" : { - "it" : { - "stringUnit" : { - "state" : "translated", - "value" : "Aiuto per lo sviluppo di app" - } - }, - "sr" : { - "stringUnit" : { - "state" : "translated", - "value" : "Помози при развоју апликације" - } - }, - "zh-Hans" : { - "stringUnit" : { - "state" : "translated", - "value" : "帮助开发应用程序" - } - }, - "zh-Hant-TW" : { - "stringUnit" : { - "state" : "translated", - "value" : "幫助App開發" - } - } - } - }, "Hide alerts" : { "localizations" : { "it" : { @@ -24353,17 +24328,6 @@ } } }, - "Replying to a message" : { - "extractionState" : "stale", - "localizations" : { - "zh-Hant-TW" : { - "stringUnit" : { - "state" : "translated", - "value" : "正在回覆訊息" - } - } - } - }, "Request Legacy Admin: %@" : { "localizations" : { "it" : { @@ -28970,6 +28934,9 @@ } } } + }, + "Sponsor App Development" : { + }, "Spread Factor" : { "localizations" : { diff --git a/Meshtastic/Helpers/MeshPackets.swift b/Meshtastic/Helpers/MeshPackets.swift index fe290aa0..24b93a02 100644 --- a/Meshtastic/Helpers/MeshPackets.swift +++ b/Meshtastic/Helpers/MeshPackets.swift @@ -711,7 +711,7 @@ func telemetryPacket(packet: MeshPacket, connectedNode: Int64, context: NSManage if let telemetryMessage = try? Telemetry(serializedBytes: packet.decoded.payload) { let logString = String.localizedStringWithFormat("Telemetry received for: %@".localized, String(packet.from)) Logger.mesh.info("📈 \(logString, privacy: .public)") - if telemetryMessage.variant != Telemetry.OneOf_Variant.deviceMetrics(telemetryMessage.deviceMetrics) && telemetryMessage.variant != Telemetry.OneOf_Variant.environmentMetrics(telemetryMessage.environmentMetrics) && telemetryMessage.variant != Telemetry.OneOf_Variant.localStats(telemetryMessage.localStats) && telemetryMessage.variant != Telemetry.OneOf_Variant.powerMetrics(telemetryMessage.powerMetrics) { + if telemetryMessage.variant != Telemetry.OneOf_Variant.deviceMetrics(telemetryMessage.deviceMetrics) && telemetryMessage.variant != Telemetry.OneOf_Variant.environmentMetrics(telemetryMessage.environmentMetrics) && telemetryMessage.variant != Telemetry.OneOf_Variant.localStats(telemetryMessage.localStats) && telemetryMessage.variant != Telemetry.OneOf_Variant.powerMetrics(telemetryMessage.powerMetrics) { /// Other unhandled telemetry packets return } @@ -979,79 +979,79 @@ func textMessageAppPacket( try context.save() Logger.data.info("💾 Saved a new message for \(newMessage.messageId, privacy: .public)") messageSaved = true - - if messageSaved { - if packet.decoded.portnum == PortNum.detectionSensorApp && !UserDefaults.enableDetectionNotifications { - return - } - if newMessage.fromUser != nil && newMessage.toUser != nil { - // Set Unread Message Indicators - if packet.to == connectedNode { - appState.unreadDirectMessages = newMessage.toUser?.unreadMessages ?? 0 - } - if !(newMessage.fromUser?.mute ?? false) { - // Create an iOS Notification for the received DM message - let manager = LocalNotificationManager() - manager.notifications = [ - Notification( - id: ("notification.id.\(newMessage.messageId)"), - title: "\(newMessage.fromUser?.longName ?? "Unknown".localized)", - subtitle: "AKA \(newMessage.fromUser?.shortName ?? "?")", - content: messageText!, - target: "messages", - path: "meshtastic:///messages?userNum=\(newMessage.fromUser?.num ?? 0)&messageId=\(newMessage.messageId)", - messageId: newMessage.messageId, - channel: newMessage.channel, - userNum: Int64(packet.from), - critical: critical - ) - ] - manager.schedule() - Logger.services.debug("iOS Notification Scheduled for text message from \(newMessage.fromUser?.longName ?? "Unknown".localized, privacy: .public)") - } - } else if newMessage.fromUser != nil && newMessage.toUser == nil { - let fetchMyInfoRequest = MyInfoEntity.fetchRequest() - fetchMyInfoRequest.predicate = NSPredicate(format: "myNodeNum == %lld", Int64(connectedNode)) - do { - let fetchedMyInfo = try context.fetch(fetchMyInfoRequest) - if !fetchedMyInfo.isEmpty { - appState.unreadChannelMessages = fetchedMyInfo[0].unreadMessages - for channel in (fetchedMyInfo[0].channels?.array ?? []) as? [ChannelEntity] ?? [] { - if channel.index == newMessage.channel { - context.refresh(channel, mergeChanges: true) - } - if channel.index == newMessage.channel && !channel.mute && UserDefaults.channelMessageNotifications { - // Create an iOS Notification for the received channel message - let manager = LocalNotificationManager() - manager.notifications = [ - Notification( - id: ("notification.id.\(newMessage.messageId)"), - title: "\(newMessage.fromUser?.longName ?? "Unknown".localized)", - subtitle: "AKA \(newMessage.fromUser?.shortName ?? "?")", - content: messageText!, - target: "messages", - path: "meshtastic:///messages?channelId=\(newMessage.channel)&messageId=\(newMessage.messageId)", - messageId: newMessage.messageId, - channel: newMessage.channel, - userNum: Int64(newMessage.fromUser?.userId ?? "0"), - critical: critical - ) - ] - manager.schedule() - Logger.services.debug("iOS Notification Scheduled for text message from \(newMessage.fromUser?.longName ?? "Unknown".localized, privacy: .public)") - } - } - } - } catch { - // Handle error - } - } - } } catch { context.rollback() let nsError = error as NSError Logger.data.error("Failed to save new MessageEntity \(nsError, privacy: .public)") } + // Send notifications if the message saved properly to core data + if messageSaved { + if packet.decoded.portnum == PortNum.detectionSensorApp && !UserDefaults.enableDetectionNotifications { + return + } + if newMessage.fromUser != nil && newMessage.toUser != nil { + // Set Unread Message Indicators + if packet.to == connectedNode { + appState.unreadDirectMessages = newMessage.toUser?.unreadMessages ?? 0 + } + if !(newMessage.fromUser?.mute ?? false) { + // Create an iOS Notification for the received DM message + let manager = LocalNotificationManager() + manager.notifications = [ + Notification( + id: ("notification.id.\(newMessage.messageId)"), + title: "\(newMessage.fromUser?.longName ?? "Unknown".localized)", + subtitle: "AKA \(newMessage.fromUser?.shortName ?? "?")", + content: messageText!, + target: "messages", + path: "meshtastic:///messages?userNum=\(newMessage.fromUser?.num ?? 0)&messageId=\(newMessage.messageId)", + messageId: newMessage.messageId, + channel: newMessage.channel, + userNum: Int64(packet.from), + critical: critical + ) + ] + manager.schedule() + Logger.services.debug("iOS Notification Scheduled for text message from \(newMessage.fromUser?.longName ?? "Unknown".localized, privacy: .public)") + } + } else if newMessage.fromUser != nil && newMessage.toUser == nil { + let fetchMyInfoRequest = MyInfoEntity.fetchRequest() + fetchMyInfoRequest.predicate = NSPredicate(format: "myNodeNum == %lld", Int64(connectedNode)) + do { + let fetchedMyInfo = try context.fetch(fetchMyInfoRequest) + if !fetchedMyInfo.isEmpty { + appState.unreadChannelMessages = fetchedMyInfo[0].unreadMessages + for channel in (fetchedMyInfo[0].channels?.array ?? []) as? [ChannelEntity] ?? [] { + if channel.index == newMessage.channel { + context.refresh(channel, mergeChanges: true) + } + if channel.index == newMessage.channel && !channel.mute && UserDefaults.channelMessageNotifications { + // Create an iOS Notification for the received channel message + let manager = LocalNotificationManager() + manager.notifications = [ + Notification( + id: ("notification.id.\(newMessage.messageId)"), + title: "\(newMessage.fromUser?.longName ?? "Unknown".localized)", + subtitle: "AKA \(newMessage.fromUser?.shortName ?? "?")", + content: messageText!, + target: "messages", + path: "meshtastic:///messages?channelId=\(newMessage.channel)&messageId=\(newMessage.messageId)", + messageId: newMessage.messageId, + channel: newMessage.channel, + userNum: Int64(newMessage.fromUser?.userId ?? "0"), + critical: critical + ) + ] + manager.schedule() + Logger.services.debug("iOS Notification Scheduled for text message from \(newMessage.fromUser?.longName ?? "Unknown".localized, privacy: .public)") + } + } + } + } catch { + // Handle error + } + } + } } catch { Logger.data.error("Fetch Message To and From Users Error") } diff --git a/Meshtastic/Views/Settings/About.swift b/Meshtastic/Views/Settings/About.swift index e65a4c64..98355864 100644 --- a/Meshtastic/Views/Settings/About.swift +++ b/Meshtastic/Views/Settings/About.swift @@ -38,7 +38,9 @@ struct AboutMeshtastic: View { } } } - Link("Help with App Development", destination: URL(string: "https://github.com/meshtastic/Meshtastic-Apple")!) + Link("Sponsor App Development", destination: URL(string: "https://github.com/sponsors/garthvh")!) + .font(.title2) + Link("GitHub Repository", destination: URL(string: "https://github.com/meshtastic/Meshtastic-Apple")!) .font(.title2) Button("Review the app") { if let scene = UIApplication.shared.connectedScenes From 4d6e8c8940cccea52dfb2e2534d3361b9d42a849 Mon Sep 17 00:00:00 2001 From: Benjamin Faershtein <119711889+RCGV1@users.noreply.github.com> Date: Sat, 14 Jun 2025 08:58:17 -0700 Subject: [PATCH 111/213] fixed some logic --- Meshtastic/Helpers/BLEManager.swift | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/Meshtastic/Helpers/BLEManager.swift b/Meshtastic/Helpers/BLEManager.swift index 008fe3e8..bc5fd6b3 100644 --- a/Meshtastic/Helpers/BLEManager.swift +++ b/Meshtastic/Helpers/BLEManager.swift @@ -1029,9 +1029,7 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate sendWantConfig() } - if decodedInfo.configCompleteID != 0 && decodedInfo.configCompleteID == 69421 { - Logger.mesh.info("🤜 [BLE] Want Config DB Complete. ID:\(decodedInfo.configCompleteID, privacy: .public)") - } + // MARK: Share Location Position Update Timer // Use context to pass the radio name with the timer @@ -1045,6 +1043,9 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate } return } + if decodedInfo.configCompleteID != 0 && decodedInfo.configCompleteID == 69421 { + Logger.mesh.info("🤜 [BLE] Want Config DB Complete. ID:\(decodedInfo.configCompleteID, privacy: .public)") + } case FROMNUM_UUID: Logger.services.info("🗞️ [BLE] (Notify) characteristic value will be read next") From 5a854725ee53233ae8478432c8a433a936976cd5 Mon Sep 17 00:00:00 2001 From: Benjamin Faershtein <119711889+RCGV1@users.noreply.github.com> Date: Sat, 14 Jun 2025 09:10:07 -0700 Subject: [PATCH 112/213] added nonce variables --- Meshtastic/Helpers/BLEManager.swift | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/Meshtastic/Helpers/BLEManager.swift b/Meshtastic/Helpers/BLEManager.swift index bc5fd6b3..4c093547 100644 --- a/Meshtastic/Helpers/BLEManager.swift +++ b/Meshtastic/Helpers/BLEManager.swift @@ -52,6 +52,9 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate let FROMNUM_UUID = CBUUID(string: "0xED9DA18C-A800-4F66-A670-AA7547E34453") let LEGACY_LOGRADIO_UUID = CBUUID(string: "0x6C6FD238-78FA-436B-AACF-15C5BE1EF2E2") let LOGRADIO_UUID = CBUUID(string: "0x5a3d6e49-06e6-4423-9944-e9de8cdf9547") + + let NONCE_ONLY_CONFIG = 69420 + let NONCE_ONLY_DB = 69421 // MARK: init private override init() { @@ -511,9 +514,9 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate Logger.mesh.info("🛎️ \(logString, privacy: .public)") // BLE Characteristics discovered, issue wantConfig var toRadio: ToRadio = ToRadio() - configNonce = UInt32(69421) + configNonce = UInt32(NONCE_ONLY_DB) if !isSubscribed { - configNonce = UInt32(69420) // Get config first + configNonce = UInt32(NONCE_ONLY_CONFIG) // Get config first } toRadio.wantConfigID = configNonce guard let binaryData: Data = try? toRadio.serializedData() else { @@ -985,7 +988,7 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate Logger.mesh.warning("🕸️ MESH PACKET received for Key Verification App UNHANDLED \((try? decodedInfo.packet.jsonString()) ?? "JSON Decode Failure", privacy: .public)") } - if decodedInfo.configCompleteID != 0 && decodedInfo.configCompleteID == 69420 { + if decodedInfo.configCompleteID != 0 && decodedInfo.configCompleteID == NONCE_ONLY_CONFIG { invalidVersion = false lastConnectionError = "" isSubscribed = true @@ -1043,7 +1046,7 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate } return } - if decodedInfo.configCompleteID != 0 && decodedInfo.configCompleteID == 69421 { + if decodedInfo.configCompleteID != 0 && decodedInfo.configCompleteID == NONCE_ONLY_DB { Logger.mesh.info("🤜 [BLE] Want Config DB Complete. ID:\(decodedInfo.configCompleteID, privacy: .public)") } From 75cf037cfe469ed036755a72a53d181041cbbe48 Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Sat, 14 Jun 2025 14:25:46 -0700 Subject: [PATCH 113/213] Remove more remnants of the old admin channel --- Localizable.xcstrings | 3 + Meshtastic/AppIntents/RestartNodeIntent.swift | 5 +- .../AppIntents/ShutDownNodeIntent.swift | 5 +- Meshtastic/Helpers/BLEManager.swift | 175 +++++++----------- Meshtastic/Helpers/MeshPackets.swift | 3 - Meshtastic/Views/Bluetooth/Connect.swift | 2 +- .../Views/Nodes/Helpers/NodeDetail.swift | 7 +- .../Settings/Config/BluetoothConfig.swift | 4 +- .../Views/Settings/Config/DeviceConfig.swift | 4 +- .../Views/Settings/Config/DisplayConfig.swift | 4 +- .../Views/Settings/Config/LoRaConfig.swift | 4 +- .../Config/Module/AmbientLightingConfig.swift | 4 +- .../Config/Module/CannedMessagesConfig.swift | 6 +- .../Config/Module/DetectionSensorConfig.swift | 4 +- .../Module/ExternalNotificationConfig.swift | 4 +- .../Settings/Config/Module/MQTTConfig.swift | 4 +- .../Config/Module/PaxCounterConfig.swift | 5 +- .../Config/Module/RangeTestConfig.swift | 4 +- .../Settings/Config/Module/RtttlConfig.swift | 4 +- .../Settings/Config/Module/SerialConfig.swift | 4 +- .../Config/Module/StoreForwardConfig.swift | 4 +- .../Config/Module/TelemetryConfig.swift | 4 +- .../Views/Settings/Config/NetworkConfig.swift | 6 +- .../Settings/Config/PositionConfig.swift | 4 +- .../Views/Settings/Config/PowerConfig.swift | 5 +- .../Settings/Config/SecurityConfig.swift | 48 ++++- Meshtastic/Views/Settings/Firmware.swift | 2 +- Meshtastic/Views/Settings/Settings.swift | 2 +- Meshtastic/Views/Settings/UserConfig.swift | 4 +- 29 files changed, 161 insertions(+), 173 deletions(-) diff --git a/Localizable.xcstrings b/Localizable.xcstrings index 401017b9..eaf40011 100644 --- a/Localizable.xcstrings +++ b/Localizable.xcstrings @@ -23949,6 +23949,9 @@ } } } + }, + "Regenerate Private Key" : { + }, "Region" : { "localizations" : { diff --git a/Meshtastic/AppIntents/RestartNodeIntent.swift b/Meshtastic/AppIntents/RestartNodeIntent.swift index 5f317a28..3859c114 100644 --- a/Meshtastic/AppIntents/RestartNodeIntent.swift +++ b/Meshtastic/AppIntents/RestartNodeIntent.swift @@ -24,11 +24,10 @@ struct RestartNodeIntent: AppIntent { if let connectedPeripheralNum = BLEManager.shared.connectedPeripheral?.num, let connectedNode = getNodeInfo(id: connectedPeripheralNum, context: PersistenceController.shared.container.viewContext), let fromUser = connectedNode.user, - let toUser = connectedNode.user, - let adminIndex = connectedNode.myInfo?.adminIndex { + let toUser = connectedNode.user { // Attempt to send shutdown, throw an error if it fails - if !BLEManager.shared.sendReboot(fromUser: fromUser, toUser: toUser, adminIndex: adminIndex) { + if !BLEManager.shared.sendReboot(fromUser: fromUser, toUser: toUser) { throw AppIntentErrors.AppIntentError.message("Failed to restart") } } else { diff --git a/Meshtastic/AppIntents/ShutDownNodeIntent.swift b/Meshtastic/AppIntents/ShutDownNodeIntent.swift index dcb43f3c..7f5acc57 100644 --- a/Meshtastic/AppIntents/ShutDownNodeIntent.swift +++ b/Meshtastic/AppIntents/ShutDownNodeIntent.swift @@ -24,11 +24,10 @@ struct ShutDownNodeIntent: AppIntent { if let connectedPeripheralNum = BLEManager.shared.connectedPeripheral?.num, let connectedNode = getNodeInfo(id: connectedPeripheralNum, context: PersistenceController.shared.container.viewContext), let fromUser = connectedNode.user, - let toUser = connectedNode.user, - let adminIndex = connectedNode.myInfo?.adminIndex { + let toUser = connectedNode.user { // Attempt to send shutdown, throw an error if it fails - if !BLEManager.shared.sendShutdown(fromUser: fromUser, toUser: toUser, adminIndex: adminIndex) { + if !BLEManager.shared.sendShutdown(fromUser: fromUser, toUser: toUser) { throw AppIntentErrors.AppIntentError.message("Failed to shut down") } } else { diff --git a/Meshtastic/Helpers/BLEManager.swift b/Meshtastic/Helpers/BLEManager.swift index c1dc7023..a49d77c4 100644 --- a/Meshtastic/Helpers/BLEManager.swift +++ b/Meshtastic/Helpers/BLEManager.swift @@ -408,7 +408,7 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate } // MARK: Protobuf Methods - func requestDeviceMetadata(fromUser: UserEntity, toUser: UserEntity, adminIndex: Int32, context: NSManagedObjectContext) -> Int64 { + func requestDeviceMetadata(fromUser: UserEntity, toUser: UserEntity, context: NSManagedObjectContext) -> Int64 { guard connectedPeripheral?.peripheral.state ?? CBPeripheralState.disconnected == CBPeripheralState.connected else { return 0 } @@ -419,7 +419,6 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate meshPacket.to = UInt32(toUser.num) meshPacket.from = UInt32(fromUser.num) meshPacket.priority = MeshPacket.Priority.reliable - meshPacket.channel = UInt32(adminIndex) meshPacket.wantAck = true var dataMessage = DataMessage() if let serializedData: Data = try? adminPacket.serializedData() { @@ -1419,7 +1418,7 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate return false } - public func sendShutdown(fromUser: UserEntity, toUser: UserEntity, adminIndex: Int32) -> Bool { + public func sendShutdown(fromUser: UserEntity, toUser: UserEntity) -> Bool { var adminPacket = AdminMessage() adminPacket.shutdownSeconds = 5 if fromUser != toUser { @@ -1431,7 +1430,6 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate meshPacket.id = UInt32.random(in: UInt32(UInt8.max).. Bool { + public func sendReboot(fromUser: UserEntity, toUser: UserEntity) -> Bool { var adminPacket = AdminMessage() adminPacket.rebootSeconds = 5 if fromUser != toUser { @@ -1459,7 +1457,6 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate meshPacket.id = UInt32.random(in: UInt32(UInt8.max).. Bool { + public func sendRebootOta(fromUser: UserEntity, toUser: UserEntity) -> Bool { var adminPacket = AdminMessage() adminPacket.rebootOtaSeconds = 5 if fromUser != toUser { @@ -1487,7 +1484,6 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate meshPacket.id = UInt32.random(in: UInt32(UInt8.max).. Int64 { + public func saveUser(config: User, fromUser: UserEntity, toUser: UserEntity) -> Int64 { var adminPacket = AdminMessage() adminPacket.setOwner = config if fromUser != toUser { @@ -1852,7 +1848,6 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate var meshPacket: MeshPacket = MeshPacket() meshPacket.to = UInt32(toUser.num) meshPacket.from = UInt32(fromUser.num) - meshPacket.channel = UInt32(adminIndex) meshPacket.id = UInt32.random(in: UInt32(UInt8.max).. Int64 { + public func saveLicensedUser(ham: HamParameters, fromUser: UserEntity, toUser: UserEntity) -> Int64 { var adminPacket = AdminMessage() adminPacket.setHamMode = ham if fromUser != toUser { @@ -2035,7 +2030,6 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate var meshPacket: MeshPacket = MeshPacket() meshPacket.to = UInt32(toUser.num) meshPacket.from = UInt32(fromUser.num) - meshPacket.channel = UInt32(adminIndex) meshPacket.id = UInt32.random(in: UInt32(UInt8.max).. Int64 { + public func saveBluetoothConfig(config: Config.BluetoothConfig, fromUser: UserEntity, toUser: UserEntity) -> Int64 { var adminPacket = AdminMessage() adminPacket.setConfig.bluetooth = config if fromUser != toUser { @@ -2061,7 +2055,6 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate var meshPacket: MeshPacket = MeshPacket() meshPacket.to = UInt32(toUser.num) meshPacket.from = UInt32(fromUser.num) - meshPacket.channel = UInt32(adminIndex) meshPacket.id = UInt32.random(in: UInt32(UInt8.max).. Int64 { + public func saveDeviceConfig(config: Config.DeviceConfig, fromUser: UserEntity, toUser: UserEntity) -> Int64 { var adminPacket = AdminMessage() adminPacket.setConfig.device = config @@ -2092,7 +2085,6 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate var meshPacket: MeshPacket = MeshPacket() meshPacket.to = UInt32(toUser.num) meshPacket.from = UInt32(fromUser.num) - meshPacket.channel = UInt32(adminIndex) meshPacket.id = UInt32.random(in: UInt32(UInt8.max).. Int64 { + public func saveDisplayConfig(config: Config.DisplayConfig, fromUser: UserEntity, toUser: UserEntity) -> Int64 { var adminPacket = AdminMessage() adminPacket.setConfig.display = config if fromUser != toUser { @@ -2120,9 +2112,6 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate var meshPacket: MeshPacket = MeshPacket() meshPacket.to = UInt32(toUser.num) meshPacket.from = UInt32(fromUser.num) - if adminIndex > 0 { - meshPacket.channel = UInt32(adminIndex) - } meshPacket.id = UInt32.random(in: UInt32(UInt8.max).. Int64 { + public func saveLoRaConfig(config: Config.LoRaConfig, fromUser: UserEntity, toUser: UserEntity) -> Int64 { var adminPacket = AdminMessage() adminPacket.setConfig.lora = config @@ -2151,7 +2140,6 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate var meshPacket: MeshPacket = MeshPacket() meshPacket.to = UInt32(toUser.num) meshPacket.from = UInt32(fromUser.num) - meshPacket.channel = UInt32(adminIndex) meshPacket.id = UInt32.random(in: UInt32(UInt8.max).. Int64 { + public func savePositionConfig(config: Config.PositionConfig, fromUser: UserEntity, toUser: UserEntity) -> Int64 { var adminPacket = AdminMessage() adminPacket.setConfig.position = config @@ -2181,7 +2169,6 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate var meshPacket: MeshPacket = MeshPacket() meshPacket.to = UInt32(toUser.num) meshPacket.from = UInt32(fromUser.num) - meshPacket.channel = UInt32(adminIndex) meshPacket.id = UInt32.random(in: UInt32(UInt8.max).. Int64 { + public func savePowerConfig(config: Config.PowerConfig, fromUser: UserEntity, toUser: UserEntity) -> Int64 { var adminPacket = AdminMessage() adminPacket.setConfig.power = config @@ -2212,7 +2199,6 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate var meshPacket: MeshPacket = MeshPacket() meshPacket.to = UInt32(toUser.num) meshPacket.from = UInt32(fromUser.num) - meshPacket.channel = UInt32(adminIndex) meshPacket.id = UInt32.random(in: UInt32(UInt8.max).. Int64 { + public func saveNetworkConfig(config: Config.NetworkConfig, fromUser: UserEntity, toUser: UserEntity) -> Int64 { var adminPacket = AdminMessage() adminPacket.setConfig.network = config @@ -2245,7 +2231,6 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate var meshPacket: MeshPacket = MeshPacket() meshPacket.to = UInt32(toUser.num) meshPacket.from = UInt32(fromUser.num) - meshPacket.channel = UInt32(adminIndex) meshPacket.id = UInt32.random(in: UInt32(UInt8.max).. Int64 { + public func saveSecurityConfig(config: Config.SecurityConfig, fromUser: UserEntity, toUser: UserEntity) -> Int64 { var adminPacket = AdminMessage() adminPacket.setConfig.security = config @@ -2278,7 +2263,6 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate var meshPacket: MeshPacket = MeshPacket() meshPacket.to = UInt32(toUser.num) meshPacket.from = UInt32(fromUser.num) - meshPacket.channel = UInt32(adminIndex) meshPacket.id = UInt32.random(in: UInt32(UInt8.max).. Int64 { + public func saveAmbientLightingModuleConfig(config: ModuleConfig.AmbientLightingConfig, fromUser: UserEntity, toUser: UserEntity) -> Int64 { var adminPacket = AdminMessage() adminPacket.setModuleConfig.ambientLighting = config @@ -2311,7 +2295,6 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate var meshPacket: MeshPacket = MeshPacket() meshPacket.to = UInt32(toUser.num) meshPacket.from = UInt32(fromUser.num) - meshPacket.channel = UInt32(adminIndex) meshPacket.id = UInt32.random(in: UInt32(UInt8.max).. Int64 { + public func saveCannedMessageModuleConfig(config: ModuleConfig.CannedMessageConfig, fromUser: UserEntity, toUser: UserEntity) -> Int64 { var adminPacket = AdminMessage() adminPacket.setModuleConfig.cannedMessage = config @@ -2343,7 +2326,6 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate var meshPacket: MeshPacket = MeshPacket() meshPacket.to = UInt32(toUser.num) meshPacket.from = UInt32(fromUser.num) - meshPacket.channel = UInt32(adminIndex) meshPacket.id = UInt32.random(in: UInt32(UInt8.max).. Int64 { + public func saveCannedMessageModuleMessages(messages: String, fromUser: UserEntity, toUser: UserEntity) -> Int64 { var adminPacket = AdminMessage() adminPacket.setCannedMessageModuleMessages = messages @@ -2375,7 +2357,6 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate var meshPacket: MeshPacket = MeshPacket() meshPacket.to = UInt32(toUser.num) meshPacket.from = UInt32(fromUser.num) - meshPacket.channel = UInt32(adminIndex) meshPacket.id = UInt32.random(in: UInt32(UInt8.max).. Int64 { + public func saveDetectionSensorModuleConfig(config: ModuleConfig.DetectionSensorConfig, fromUser: UserEntity, toUser: UserEntity) -> Int64 { var adminPacket = AdminMessage() adminPacket.setModuleConfig.detectionSensor = config @@ -2409,7 +2390,6 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate meshPacket.id = UInt32.random(in: UInt32(UInt8.max).. Int64 { + public func saveExternalNotificationModuleConfig(config: ModuleConfig.ExternalNotificationConfig, fromUser: UserEntity, toUser: UserEntity) -> Int64 { var adminPacket = AdminMessage() adminPacket.setModuleConfig.externalNotification = config @@ -2439,7 +2419,6 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate var meshPacket: MeshPacket = MeshPacket() meshPacket.to = UInt32(toUser.num) meshPacket.from = UInt32(fromUser.num) - meshPacket.channel = UInt32(adminIndex) meshPacket.id = UInt32.random(in: UInt32(UInt8.max).. Int64 { + public func savePaxcounterModuleConfig(config: ModuleConfig.PaxcounterConfig, fromUser: UserEntity, toUser: UserEntity) -> Int64 { var adminPacket = AdminMessage() adminPacket.setModuleConfig.paxcounter = config @@ -2470,7 +2449,6 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate var meshPacket: MeshPacket = MeshPacket() meshPacket.to = UInt32(toUser.num) meshPacket.from = UInt32(fromUser.num) - meshPacket.channel = UInt32(adminIndex) meshPacket.id = UInt32.random(in: UInt32(UInt8.max).. Int64 { + public func saveRtttlConfig(ringtone: String, fromUser: UserEntity, toUser: UserEntity) -> Int64 { var adminPacket = AdminMessage() adminPacket.setRingtoneMessage = ringtone @@ -2503,7 +2481,6 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate meshPacket.id = UInt32.random(in: UInt32(UInt8.max).. Int64 { + public func saveMQTTConfig(config: ModuleConfig.MQTTConfig, fromUser: UserEntity, toUser: UserEntity) -> Int64 { var adminPacket = AdminMessage() adminPacket.setModuleConfig.mqtt = config @@ -2535,7 +2512,6 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate meshPacket.id = UInt32.random(in: UInt32(UInt8.max).. Int64 { + public func saveRangeTestModuleConfig(config: ModuleConfig.RangeTestConfig, fromUser: UserEntity, toUser: UserEntity) -> Int64 { var adminPacket = AdminMessage() adminPacket.setModuleConfig.rangeTest = config @@ -2567,7 +2543,6 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate meshPacket.id = UInt32.random(in: UInt32(UInt8.max).. Int64 { + public func saveSerialModuleConfig(config: ModuleConfig.SerialConfig, fromUser: UserEntity, toUser: UserEntity) -> Int64 { var adminPacket = AdminMessage() adminPacket.setModuleConfig.serial = config @@ -2599,7 +2574,6 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate meshPacket.id = UInt32.random(in: UInt32(UInt8.max).. Int64 { + public func saveStoreForwardModuleConfig(config: ModuleConfig.StoreForwardConfig, fromUser: UserEntity, toUser: UserEntity) -> Int64 { var adminPacket = AdminMessage() adminPacket.setModuleConfig.storeForward = config @@ -2630,7 +2604,6 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate meshPacket.id = UInt32.random(in: UInt32(UInt8.max).. Int64 { + public func saveTelemetryModuleConfig(config: ModuleConfig.TelemetryConfig, fromUser: UserEntity, toUser: UserEntity) -> Int64 { var adminPacket = AdminMessage() adminPacket.setModuleConfig.telemetry = config @@ -2661,7 +2634,6 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate meshPacket.id = UInt32.random(in: UInt32(UInt8.max).. Bool { + public func requestBluetoothConfig(fromUser: UserEntity, toUser: UserEntity) -> Bool { var adminPacket = AdminMessage() adminPacket.getConfigRequest = AdminMessage.ConfigType.bluetoothConfig @@ -2766,7 +2738,6 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate meshPacket.from = UInt32(fromUser.num) meshPacket.id = UInt32.random(in: UInt32(UInt8.max).. Bool { + public func requestDeviceConfig(fromUser: UserEntity, toUser: UserEntity) -> Bool { var adminPacket = AdminMessage() adminPacket.getConfigRequest = AdminMessage.ConfigType.deviceConfig @@ -2796,7 +2767,6 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate meshPacket.from = UInt32(fromUser.num) meshPacket.id = UInt32.random(in: UInt32(UInt8.max).. Bool { + public func requestDisplayConfig(fromUser: UserEntity, toUser: UserEntity) -> Bool { var adminPacket = AdminMessage() adminPacket.getConfigRequest = AdminMessage.ConfigType.displayConfig @@ -2826,7 +2796,6 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate meshPacket.from = UInt32(fromUser.num) meshPacket.id = UInt32.random(in: UInt32(UInt8.max).. Bool { + public func requestLoRaConfig(fromUser: UserEntity, toUser: UserEntity) -> Bool { var adminPacket = AdminMessage() adminPacket.getConfigRequest = AdminMessage.ConfigType.loraConfig @@ -2856,7 +2825,6 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate meshPacket.from = UInt32(fromUser.num) meshPacket.id = UInt32.random(in: UInt32(UInt8.max).. Bool { + public func requestNetworkConfig(fromUser: UserEntity, toUser: UserEntity) -> Bool { var adminPacket = AdminMessage() adminPacket.getConfigRequest = AdminMessage.ConfigType.networkConfig @@ -2888,7 +2856,6 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate meshPacket.from = UInt32(fromUser.num) meshPacket.id = UInt32.random(in: UInt32(UInt8.max).. Bool { + public func requestPositionConfig(fromUser: UserEntity, toUser: UserEntity) -> Bool { var adminPacket = AdminMessage() adminPacket.getConfigRequest = AdminMessage.ConfigType.positionConfig @@ -2917,7 +2884,6 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate meshPacket.from = UInt32(fromUser.num) meshPacket.id = UInt32.random(in: UInt32(UInt8.max).. Bool { + public func requestPowerConfig(fromUser: UserEntity, toUser: UserEntity) -> Bool { var adminPacket = AdminMessage() adminPacket.getConfigRequest = AdminMessage.ConfigType.powerConfig @@ -2946,7 +2912,6 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate meshPacket.from = UInt32(fromUser.num) meshPacket.id = UInt32.random(in: UInt32(UInt8.max).. Bool { + public func requestSecurityConfig(fromUser: UserEntity, toUser: UserEntity) -> Bool { var adminPacket = AdminMessage() adminPacket.getConfigRequest = AdminMessage.ConfigType.securityConfig @@ -2975,7 +2940,6 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate meshPacket.from = UInt32(fromUser.num) meshPacket.id = UInt32.random(in: UInt32(UInt8.max).. Bool { + public func requestAmbientLightingConfig(fromUser: UserEntity, toUser: UserEntity) -> Bool { var adminPacket = AdminMessage() adminPacket.getModuleConfigRequest = AdminMessage.ModuleConfigType.ambientlightingConfig @@ -3004,7 +2968,6 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate meshPacket.from = UInt32(fromUser.num) meshPacket.id = UInt32.random(in: UInt32(UInt8.max).. Bool { + public func requestCannedMessagesModuleConfig(fromUser: UserEntity, toUser: UserEntity) -> Bool { var adminPacket = AdminMessage() adminPacket.getModuleConfigRequest = AdminMessage.ModuleConfigType.cannedmsgConfig @@ -3033,7 +2996,6 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate meshPacket.from = UInt32(fromUser.num) meshPacket.id = UInt32.random(in: UInt32(UInt8.max).. Bool { + public func requestExternalNotificationModuleConfig(fromUser: UserEntity, toUser: UserEntity) -> Bool { var adminPacket = AdminMessage() adminPacket.getModuleConfigRequest = AdminMessage.ModuleConfigType.extnotifConfig @@ -3062,7 +3024,6 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate meshPacket.from = UInt32(fromUser.num) meshPacket.id = UInt32.random(in: UInt32(UInt8.max).. Bool { + public func requestPaxCounterModuleConfig(fromUser: UserEntity, toUser: UserEntity) -> Bool { var adminPacket = AdminMessage() adminPacket.getModuleConfigRequest = AdminMessage.ModuleConfigType.paxcounterConfig @@ -3091,7 +3052,6 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate meshPacket.from = UInt32(fromUser.num) meshPacket.id = UInt32.random(in: UInt32(UInt8.max).. Bool { + public func requestRtttlConfig(fromUser: UserEntity, toUser: UserEntity) -> Bool { var adminPacket = AdminMessage() adminPacket.getRingtoneRequest = true @@ -3120,7 +3080,6 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate meshPacket.from = UInt32(fromUser.num) meshPacket.id = UInt32.random(in: UInt32(UInt8.max).. Bool { + public func requestRangeTestModuleConfig(fromUser: UserEntity, toUser: UserEntity) -> Bool { var adminPacket = AdminMessage() adminPacket.getModuleConfigRequest = AdminMessage.ModuleConfigType.rangetestConfig @@ -3149,7 +3108,6 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate meshPacket.from = UInt32(fromUser.num) meshPacket.id = UInt32.random(in: UInt32(UInt8.max).. Bool { + public func requestMqttModuleConfig(fromUser: UserEntity, toUser: UserEntity) -> Bool { var adminPacket = AdminMessage() adminPacket.getModuleConfigRequest = AdminMessage.ModuleConfigType.mqttConfig @@ -3178,7 +3136,6 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate meshPacket.from = UInt32(fromUser.num) meshPacket.id = UInt32.random(in: UInt32(UInt8.max).. Bool { + public func requestDetectionSensorModuleConfig(fromUser: UserEntity, toUser: UserEntity) -> Bool { var adminPacket = AdminMessage() adminPacket.getModuleConfigRequest = AdminMessage.ModuleConfigType.detectionsensorConfig @@ -3207,7 +3164,6 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate meshPacket.from = UInt32(fromUser.num) meshPacket.id = UInt32.random(in: UInt32(UInt8.max).. Bool { + public func requestSerialModuleConfig(fromUser: UserEntity, toUser: UserEntity) -> Bool { var adminPacket = AdminMessage() adminPacket.getModuleConfigRequest = AdminMessage.ModuleConfigType.serialConfig @@ -3236,7 +3192,6 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate meshPacket.from = UInt32(fromUser.num) meshPacket.id = UInt32.random(in: UInt32(UInt8.max).. Bool { + public func requestStoreAndForwardModuleConfig(fromUser: UserEntity, toUser: UserEntity) -> Bool { var adminPacket = AdminMessage() adminPacket.getModuleConfigRequest = AdminMessage.ModuleConfigType.storeforwardConfig @@ -3265,7 +3220,6 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate meshPacket.from = UInt32(fromUser.num) meshPacket.id = UInt32.random(in: UInt32(UInt8.max).. Bool { + public func requestTelemetryModuleConfig(fromUser: UserEntity, toUser: UserEntity) -> Bool { var adminPacket = AdminMessage() adminPacket.getModuleConfigRequest = AdminMessage.ModuleConfigType.telemetryConfig @@ -3295,7 +3249,6 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate meshPacket.from = UInt32(fromUser.num) meshPacket.id = UInt32.random(in: UInt32(UInt8.max).. 0 { @@ -543,8 +542,7 @@ struct NodeDetail: View { Button("Shutdown Node?", role: .destructive) { if !bleManager.sendShutdown( fromUser: connectedNode.user!, - toUser: node.user!, - adminIndex: connectedNode.myInfo!.adminIndex + toUser: node.user! ) { Logger.mesh.warning("Shutdown Failed") } @@ -566,8 +564,7 @@ struct NodeDetail: View { Button("Reboot node?", role: .destructive) { if !bleManager.sendReboot( fromUser: connectedNode.user!, - toUser: node.user!, - adminIndex: connectedNode.myInfo!.adminIndex + toUser: node.user! ) { Logger.mesh.warning("Reboot Failed") } diff --git a/Meshtastic/Views/Settings/Config/BluetoothConfig.swift b/Meshtastic/Views/Settings/Config/BluetoothConfig.swift index 81b43499..be6b9522 100644 --- a/Meshtastic/Views/Settings/Config/BluetoothConfig.swift +++ b/Meshtastic/Views/Settings/Config/BluetoothConfig.swift @@ -80,7 +80,7 @@ struct BluetoothConfig: View { bc.enabled = enabled bc.mode = BluetoothModes(rawValue: mode)?.protoEnumValue() ?? Config.BluetoothConfig.PairingMode.randomPin bc.fixedPin = UInt32(fixedPin) ?? 123456 - let adminMessageId = bleManager.saveBluetoothConfig(config: bc, fromUser: connectedNode.user!, toUser: node!.user!, adminIndex: connectedNode.myInfo?.adminIndex ?? 0) + let adminMessageId = bleManager.saveBluetoothConfig(config: bc, fromUser: connectedNode.user!, toUser: node!.user!) if adminMessageId > 0 { // Should show a saved successfully alert once I know that to be true // for now just disable the button after a successful save @@ -111,7 +111,7 @@ struct BluetoothConfig: View { let expiration = node.sessionExpiration ?? Date() if expiration < Date() || node.bluetoothConfig == nil { Logger.mesh.info("⚙️ Empty or expired bluetooth config requesting via PKI admin") - _ = bleManager.requestBluetoothConfig(fromUser: connectedNode.user!, toUser: node.user!, adminIndex: connectedNode.myInfo?.adminIndex ?? 0) + _ = bleManager.requestBluetoothConfig(fromUser: connectedNode.user!, toUser: node.user!) } } else { /// Legacy Administration diff --git a/Meshtastic/Views/Settings/Config/DeviceConfig.swift b/Meshtastic/Views/Settings/Config/DeviceConfig.swift index bb9b6916..834dcc0f 100644 --- a/Meshtastic/Views/Settings/Config/DeviceConfig.swift +++ b/Meshtastic/Views/Settings/Config/DeviceConfig.swift @@ -245,7 +245,7 @@ struct DeviceConfig: View { dc.disableTripleClick = !tripleClickAsAdHocPing dc.tzdef = tzdef dc.ledHeartbeatDisabled = !ledHeartbeatEnabled - let adminMessageId = bleManager.saveDeviceConfig(config: dc, fromUser: connectedNode!.user!, toUser: node!.user!, adminIndex: connectedNode?.myInfo?.adminIndex ?? 0) + let adminMessageId = bleManager.saveDeviceConfig(config: dc, fromUser: connectedNode!.user!, toUser: node!.user!) if adminMessageId > 0 { // Should show a saved successfully alert once I know that to be true // for now just disable the button after a successful save @@ -278,7 +278,7 @@ struct DeviceConfig: View { let expiration = node.sessionExpiration ?? Date() if expiration < Date() || node.deviceConfig == nil { Logger.mesh.info("⚙️ Empty or expired device config requesting via PKI admin") - _ = bleManager.requestDeviceConfig(fromUser: connectedNode.user!, toUser: node.user!, adminIndex: connectedNode.myInfo?.adminIndex ?? 0) + _ = bleManager.requestDeviceConfig(fromUser: connectedNode.user!, toUser: node.user!) } } else { if node.deviceConfig == nil { diff --git a/Meshtastic/Views/Settings/Config/DisplayConfig.swift b/Meshtastic/Views/Settings/Config/DisplayConfig.swift index c9029408..20a7c521 100644 --- a/Meshtastic/Views/Settings/Config/DisplayConfig.swift +++ b/Meshtastic/Views/Settings/Config/DisplayConfig.swift @@ -142,7 +142,7 @@ struct DisplayConfig: View { dc.displaymode = DisplayModes(rawValue: displayMode)!.protoEnumValue() dc.units = Units(rawValue: units)!.protoEnumValue() - let adminMessageId = bleManager.saveDisplayConfig(config: dc, fromUser: connectedNode!.user!, toUser: node!.user!, adminIndex: connectedNode?.myInfo?.adminIndex ?? 0) + let adminMessageId = bleManager.saveDisplayConfig(config: dc, fromUser: connectedNode!.user!, toUser: node!.user!) if adminMessageId > 0 { // Should show a saved successfully alert once I know that to be true @@ -174,7 +174,7 @@ struct DisplayConfig: View { let expiration = node.sessionExpiration ?? Date() if expiration < Date() || node.displayConfig == nil { Logger.mesh.info("⚙️ Empty or expired display config requesting via PKI admin") - _ = bleManager.requestDisplayConfig(fromUser: connectedNode.user!, toUser: node.user!, adminIndex: connectedNode.myInfo?.adminIndex ?? 0) + _ = bleManager.requestDisplayConfig(fromUser: connectedNode.user!, toUser: node.user!) } } else { /// Legacy Administration diff --git a/Meshtastic/Views/Settings/Config/LoRaConfig.swift b/Meshtastic/Views/Settings/Config/LoRaConfig.swift index 452da960..3e8beb9a 100644 --- a/Meshtastic/Views/Settings/Config/LoRaConfig.swift +++ b/Meshtastic/Views/Settings/Config/LoRaConfig.swift @@ -218,7 +218,7 @@ struct LoRaConfig: View { if connectedNode?.num ?? -1 == node?.user?.num ?? 0 { UserDefaults.modemPreset = modemPreset } - let adminMessageId = bleManager.saveLoRaConfig(config: lc, fromUser: connectedNode!.user!, toUser: node!.user!, adminIndex: connectedNode?.myInfo?.adminIndex ?? 0) + let adminMessageId = bleManager.saveLoRaConfig(config: lc, fromUser: connectedNode!.user!, toUser: node!.user!) if adminMessageId > 0 { // Should show a saved successfully alert once I know that to be true // for now just disable the button after a successful save @@ -250,7 +250,7 @@ struct LoRaConfig: View { if expiration < Date() || node.loRaConfig == nil { Logger.mesh.info("⚙️ Empty or expired lora config requesting via PKI admin") if connectedNode.user != nil && node.user != nil { - _ = bleManager.requestLoRaConfig(fromUser: connectedNode.user!, toUser: node.user!, adminIndex: connectedNode.myInfo?.adminIndex ?? 0) + _ = bleManager.requestLoRaConfig(fromUser: connectedNode.user!, toUser: node.user!) } } } else { diff --git a/Meshtastic/Views/Settings/Config/Module/AmbientLightingConfig.swift b/Meshtastic/Views/Settings/Config/Module/AmbientLightingConfig.swift index efbeed70..ad3e5e3c 100644 --- a/Meshtastic/Views/Settings/Config/Module/AmbientLightingConfig.swift +++ b/Meshtastic/Views/Settings/Config/Module/AmbientLightingConfig.swift @@ -66,7 +66,7 @@ struct AmbientLightingConfig: View { al.blue = UInt32(components.blue * 255) } - let adminMessageId = bleManager.saveAmbientLightingModuleConfig(config: al, fromUser: connectedNode!.user!, toUser: node!.user!, adminIndex: connectedNode?.myInfo?.adminIndex ?? 0) + let adminMessageId = bleManager.saveAmbientLightingModuleConfig(config: al, fromUser: connectedNode!.user!, toUser: node!.user!) if adminMessageId > 0 { // Should show a saved successfully alert once I know that to be true // for now just disable the button after a successful save @@ -96,7 +96,7 @@ struct AmbientLightingConfig: View { let expiration = node.sessionExpiration ?? Date() if expiration < Date() || node.ambientLightingConfig == nil { Logger.mesh.info("⚙️ Empty or expired ambient lighting module config requesting via PKI admin") - _ = bleManager.requestAmbientLightingConfig(fromUser: connectedNode.user!, toUser: node.user!, adminIndex: connectedNode.myInfo?.adminIndex ?? 0) + _ = bleManager.requestAmbientLightingConfig(fromUser: connectedNode.user!, toUser: node.user!) } } else { /// Legacy Administration diff --git a/Meshtastic/Views/Settings/Config/Module/CannedMessagesConfig.swift b/Meshtastic/Views/Settings/Config/Module/CannedMessagesConfig.swift index 0fbcbcf8..941ed3fd 100644 --- a/Meshtastic/Views/Settings/Config/Module/CannedMessagesConfig.swift +++ b/Meshtastic/Views/Settings/Config/Module/CannedMessagesConfig.swift @@ -201,7 +201,7 @@ struct CannedMessagesConfig: View { cmc.inputbrokerEventCw = InputEventChars(rawValue: inputbrokerEventCw)!.protoEnumValue() cmc.inputbrokerEventCcw = InputEventChars(rawValue: inputbrokerEventCcw)!.protoEnumValue() cmc.inputbrokerEventPress = InputEventChars(rawValue: inputbrokerEventPress)!.protoEnumValue() - let adminMessageId = bleManager.saveCannedMessageModuleConfig(config: cmc, fromUser: node!.user!, toUser: node!.user!, adminIndex: connectedNode?.myInfo?.adminIndex ?? 0) + let adminMessageId = bleManager.saveCannedMessageModuleConfig(config: cmc, fromUser: node!.user!, toUser: node!.user!) if adminMessageId > 0 { // Should show a saved successfully alert once I know that to be true // for now just disable the button after a successful save @@ -211,7 +211,7 @@ struct CannedMessagesConfig: View { } } if hasMessagesChanges { - let adminMessageId = bleManager.saveCannedMessageModuleMessages(messages: messages, fromUser: node!.user!, toUser: node!.user!, adminIndex: connectedNode?.myInfo?.adminIndex ?? 0) + let adminMessageId = bleManager.saveCannedMessageModuleMessages(messages: messages, fromUser: node!.user!, toUser: node!.user!) if adminMessageId > 0 { // Should show a saved successfully alert once I know that to be true // for now just disable the button after a successful save @@ -244,7 +244,7 @@ struct CannedMessagesConfig: View { let expiration = node.sessionExpiration ?? Date() if expiration < Date() || node.cannedMessageConfig == nil { Logger.mesh.info("⚙️ Empty or expired canned messages module config requesting via PKI admin") - _ = bleManager.requestCannedMessagesModuleConfig(fromUser: connectedNode.user!, toUser: node.user!, adminIndex: connectedNode.myInfo?.adminIndex ?? 0) + _ = bleManager.requestCannedMessagesModuleConfig(fromUser: connectedNode.user!, toUser: node.user!) } } else { /// Legacy Administration diff --git a/Meshtastic/Views/Settings/Config/Module/DetectionSensorConfig.swift b/Meshtastic/Views/Settings/Config/Module/DetectionSensorConfig.swift index 94b96b5d..9d4b61a4 100644 --- a/Meshtastic/Views/Settings/Config/Module/DetectionSensorConfig.swift +++ b/Meshtastic/Views/Settings/Config/Module/DetectionSensorConfig.swift @@ -172,7 +172,7 @@ struct DetectionSensorConfig: View { dsc.usePullup = self.usePullup dsc.minimumBroadcastSecs = UInt32(self.minimumBroadcastSecs) dsc.stateBroadcastSecs = UInt32(self.stateBroadcastSecs) - let adminMessageId = bleManager.saveDetectionSensorModuleConfig(config: dsc, fromUser: connectedNode!.user!, toUser: node!.user!, adminIndex: connectedNode?.myInfo?.adminIndex ?? 0) + let adminMessageId = bleManager.saveDetectionSensorModuleConfig(config: dsc, fromUser: connectedNode!.user!, toUser: node!.user!) if adminMessageId > 0 { // Should show a saved successfully alert once I know that to be true // for now just disable the button after a successful save @@ -202,7 +202,7 @@ struct DetectionSensorConfig: View { let expiration = node.sessionExpiration ?? Date() if expiration < Date() || node.detectionSensorConfig == nil { Logger.mesh.info("⚙️ Empty or expired detection sensor module config requesting via PKI admin") - _ = bleManager.requestDetectionSensorModuleConfig(fromUser: connectedNode.user!, toUser: node.user!, adminIndex: connectedNode.myInfo?.adminIndex ?? 0) + _ = bleManager.requestDetectionSensorModuleConfig(fromUser: connectedNode.user!, toUser: node.user!) } } else { /// Legacy Administration diff --git a/Meshtastic/Views/Settings/Config/Module/ExternalNotificationConfig.swift b/Meshtastic/Views/Settings/Config/Module/ExternalNotificationConfig.swift index 08745f04..d0c0b13b 100644 --- a/Meshtastic/Views/Settings/Config/Module/ExternalNotificationConfig.swift +++ b/Meshtastic/Views/Settings/Config/Module/ExternalNotificationConfig.swift @@ -180,7 +180,7 @@ struct ExternalNotificationConfig: View { enc.outputMs = UInt32(outputMilliseconds) enc.usePwm = usePWM enc.useI2SAsBuzzer = useI2SAsBuzzer - let adminMessageId = bleManager.saveExternalNotificationModuleConfig(config: enc, fromUser: connectedNode!.user!, toUser: node!.user!, adminIndex: connectedNode?.myInfo?.adminIndex ?? 0) + let adminMessageId = bleManager.saveExternalNotificationModuleConfig(config: enc, fromUser: connectedNode!.user!, toUser: node!.user!) if adminMessageId > 0 { // Should show a saved successfully alert once I know that to be true // for now just disable the button after a successful save @@ -210,7 +210,7 @@ struct ExternalNotificationConfig: View { let expiration = node.sessionExpiration ?? Date() if expiration < Date() || node.externalNotificationConfig == nil { Logger.mesh.info("⚙️ Empty or expired external notificaiton module config requesting via PKI admin") - _ = bleManager.requestExternalNotificationModuleConfig(fromUser: connectedNode.user!, toUser: node.user!, adminIndex: connectedNode.myInfo?.adminIndex ?? 0) + _ = bleManager.requestExternalNotificationModuleConfig(fromUser: connectedNode.user!, toUser: node.user!) } } else { /// Legacy Administration diff --git a/Meshtastic/Views/Settings/Config/Module/MQTTConfig.swift b/Meshtastic/Views/Settings/Config/Module/MQTTConfig.swift index f06cf45c..1e9ed3da 100644 --- a/Meshtastic/Views/Settings/Config/Module/MQTTConfig.swift +++ b/Meshtastic/Views/Settings/Config/Module/MQTTConfig.swift @@ -268,7 +268,7 @@ struct MQTTConfig: View { mqtt.mapReportingEnabled = self.mapReportingEnabled mqtt.mapReportSettings.positionPrecision = UInt32(self.mapPositionPrecision) mqtt.mapReportSettings.publishIntervalSecs = UInt32(self.mapPublishIntervalSecs) - let adminMessageId = bleManager.saveMQTTConfig(config: mqtt, fromUser: connectedNode!.user!, toUser: node!.user!, adminIndex: connectedNode?.myInfo?.adminIndex ?? 0) + let adminMessageId = bleManager.saveMQTTConfig(config: mqtt, fromUser: connectedNode!.user!, toUser: node!.user!) if adminMessageId > 0 { // Should show a saved successfully alert once I know that to be true // for now just disable the button after a successful save @@ -360,7 +360,7 @@ struct MQTTConfig: View { let expiration = node.sessionExpiration ?? Date() if expiration < Date() || node.mqttConfig == nil { Logger.mesh.info("⚙️ Empty or expired mqtt module config requesting via PKI admin") - _ = bleManager.requestMqttModuleConfig(fromUser: connectedNode.user!, toUser: node.user!, adminIndex: connectedNode.myInfo?.adminIndex ?? 0) + _ = bleManager.requestMqttModuleConfig(fromUser: connectedNode.user!, toUser: node.user!) } } else { /// Legacy Administration diff --git a/Meshtastic/Views/Settings/Config/Module/PaxCounterConfig.swift b/Meshtastic/Views/Settings/Config/Module/PaxCounterConfig.swift index 7c84b406..b0101f2b 100644 --- a/Meshtastic/Views/Settings/Config/Module/PaxCounterConfig.swift +++ b/Meshtastic/Views/Settings/Config/Module/PaxCounterConfig.swift @@ -69,7 +69,7 @@ struct PaxCounterConfig: View { let expiration = node.sessionExpiration ?? Date() if expiration < Date() || node.paxCounterConfig == nil { Logger.mesh.info("⚙️ Empty or expired pax counter module config requesting via PKI admin") - _ = bleManager.requestPaxCounterModuleConfig(fromUser: connectedNode.user!, toUser: node.user!, adminIndex: connectedNode.myInfo?.adminIndex ?? 0) + _ = bleManager.requestPaxCounterModuleConfig(fromUser: connectedNode.user!, toUser: node.user!) } } else { /// Legacy Administration @@ -100,8 +100,7 @@ struct PaxCounterConfig: View { let adminMessageId = bleManager.savePaxcounterModuleConfig( config: config, fromUser: fromUser, - toUser: toUser, - adminIndex: connectedNode.myInfo?.adminIndex ?? 0 + toUser: toUser ) if adminMessageId > 0 { // Should show a saved successfully alert once I know that to be true diff --git a/Meshtastic/Views/Settings/Config/Module/RangeTestConfig.swift b/Meshtastic/Views/Settings/Config/Module/RangeTestConfig.swift index b2636967..a9b07c53 100644 --- a/Meshtastic/Views/Settings/Config/Module/RangeTestConfig.swift +++ b/Meshtastic/Views/Settings/Config/Module/RangeTestConfig.swift @@ -62,7 +62,7 @@ struct RangeTestConfig: View { rtc.enabled = enabled rtc.save = save rtc.sender = UInt32(sender) - let adminMessageId = bleManager.saveRangeTestModuleConfig(config: rtc, fromUser: connectedNode!.user!, toUser: node!.user!, adminIndex: connectedNode?.myInfo?.adminIndex ?? 0) + let adminMessageId = bleManager.saveRangeTestModuleConfig(config: rtc, fromUser: connectedNode!.user!, toUser: node!.user!) if adminMessageId > 0 { // Should show a saved successfully alert once I know that to be true // for now just disable the button after a successful save @@ -92,7 +92,7 @@ struct RangeTestConfig: View { let expiration = node.sessionExpiration ?? Date() if expiration < Date() || node.rangeTestConfig == nil { Logger.mesh.info("⚙️ Empty or expired range test module config requesting via PKI admin") - _ = bleManager.requestRangeTestModuleConfig(fromUser: connectedNode.user!, toUser: node.user!, adminIndex: connectedNode.myInfo?.adminIndex ?? 0) + _ = bleManager.requestRangeTestModuleConfig(fromUser: connectedNode.user!, toUser: node.user!) } } else { /// Legacy Administration diff --git a/Meshtastic/Views/Settings/Config/Module/RtttlConfig.swift b/Meshtastic/Views/Settings/Config/Module/RtttlConfig.swift index da30e1e4..ab1663a4 100644 --- a/Meshtastic/Views/Settings/Config/Module/RtttlConfig.swift +++ b/Meshtastic/Views/Settings/Config/Module/RtttlConfig.swift @@ -53,7 +53,7 @@ struct RtttlConfig: View { SaveConfigButton(node: node, hasChanges: $hasChanges) { let connectedNode = getNodeInfo(id: bleManager.connectedPeripheral.num, context: context) if connectedNode != nil { - let adminMessageId = bleManager.saveRtttlConfig(ringtone: ringtone.trimmingCharacters(in: .whitespacesAndNewlines), fromUser: connectedNode!.user!, toUser: node!.user!, adminIndex: connectedNode?.myInfo?.adminIndex ?? 0) + let adminMessageId = bleManager.saveRtttlConfig(ringtone: ringtone.trimmingCharacters(in: .whitespacesAndNewlines), fromUser: connectedNode!.user!, toUser: node!.user!) if adminMessageId > 0 { // Should show a saved successfully alert once I know that to be true // for now just disable the button after a successful save @@ -83,7 +83,7 @@ struct RtttlConfig: View { let expiration = node.sessionExpiration ?? Date() if expiration < Date() || node.rtttlConfig == nil { Logger.mesh.info("⚙️ Empty or expired ringtone module config requesting via PKI admin") - _ = bleManager.requestRtttlConfig(fromUser: connectedNode.user!, toUser: node.user!, adminIndex: connectedNode.myInfo?.adminIndex ?? 0) + _ = bleManager.requestRtttlConfig(fromUser: connectedNode.user!, toUser: node.user!) } } else { /// Legacy Administration diff --git a/Meshtastic/Views/Settings/Config/Module/SerialConfig.swift b/Meshtastic/Views/Settings/Config/Module/SerialConfig.swift index d1aca540..daa4c21e 100644 --- a/Meshtastic/Views/Settings/Config/Module/SerialConfig.swift +++ b/Meshtastic/Views/Settings/Config/Module/SerialConfig.swift @@ -116,7 +116,7 @@ struct SerialConfig: View { sc.overrideConsoleSerialPort = overrideConsoleSerialPort sc.mode = SerialModeTypes(rawValue: mode)!.protoEnumValue() - let adminMessageId = bleManager.saveSerialModuleConfig(config: sc, fromUser: connectedNode!.user!, toUser: node!.user!, adminIndex: connectedNode?.myInfo?.adminIndex ?? 0) + let adminMessageId = bleManager.saveSerialModuleConfig(config: sc, fromUser: connectedNode!.user!, toUser: node!.user!) if adminMessageId > 0 { // Should show a saved successfully alert once I know that to be true @@ -147,7 +147,7 @@ struct SerialConfig: View { let expiration = node.sessionExpiration ?? Date() if expiration < Date() || node.serialConfig == nil { Logger.mesh.info("⚙️ Empty or expired serial module config requesting via PKI admin") - _ = bleManager.requestSerialModuleConfig(fromUser: connectedNode.user!, toUser: node.user!, adminIndex: connectedNode.myInfo?.adminIndex ?? 0) + _ = bleManager.requestSerialModuleConfig(fromUser: connectedNode.user!, toUser: node.user!) } } else { /// Legacy Administration diff --git a/Meshtastic/Views/Settings/Config/Module/StoreForwardConfig.swift b/Meshtastic/Views/Settings/Config/Module/StoreForwardConfig.swift index c49fadf1..6f30ee33 100644 --- a/Meshtastic/Views/Settings/Config/Module/StoreForwardConfig.swift +++ b/Meshtastic/Views/Settings/Config/Module/StoreForwardConfig.swift @@ -118,7 +118,7 @@ struct StoreForwardConfig: View { sfc.records = UInt32(self.records) sfc.historyReturnMax = UInt32(self.historyReturnMax) sfc.historyReturnWindow = UInt32(self.historyReturnWindow) - let adminMessageId = bleManager.saveStoreForwardModuleConfig(config: sfc, fromUser: connectedNode!.user!, toUser: node!.user!, adminIndex: connectedNode?.myInfo?.adminIndex ?? 0) + let adminMessageId = bleManager.saveStoreForwardModuleConfig(config: sfc, fromUser: connectedNode!.user!, toUser: node!.user!) if adminMessageId > 0 { // Should show a saved successfully alert once I know that to be true // for now just disable the button after a successful save @@ -148,7 +148,7 @@ struct StoreForwardConfig: View { let expiration = node.sessionExpiration ?? Date() if expiration < Date() || node.storeForwardConfig == nil { Logger.mesh.info("⚙️ Empty or expired store & forward module config requesting via PKI admin") - _ = bleManager.requestStoreAndForwardModuleConfig(fromUser: connectedNode.user!, toUser: node.user!, adminIndex: connectedNode.myInfo?.adminIndex ?? 0) + _ = bleManager.requestStoreAndForwardModuleConfig(fromUser: connectedNode.user!, toUser: node.user!) } } else { /// Legacy Administration diff --git a/Meshtastic/Views/Settings/Config/Module/TelemetryConfig.swift b/Meshtastic/Views/Settings/Config/Module/TelemetryConfig.swift index 0ee48f86..f87e7890 100644 --- a/Meshtastic/Views/Settings/Config/Module/TelemetryConfig.swift +++ b/Meshtastic/Views/Settings/Config/Module/TelemetryConfig.swift @@ -115,7 +115,7 @@ struct TelemetryConfig: View { tc.powerMeasurementEnabled = powerMeasurementEnabled tc.powerUpdateInterval = UInt32(powerUpdateInterval) tc.powerScreenEnabled = powerScreenEnabled - let adminMessageId = bleManager.saveTelemetryModuleConfig(config: tc, fromUser: connectedNode!.user!, toUser: node!.user!, adminIndex: connectedNode?.myInfo?.adminIndex ?? 0) + let adminMessageId = bleManager.saveTelemetryModuleConfig(config: tc, fromUser: connectedNode!.user!, toUser: node!.user!) if adminMessageId > 0 { // Should show a saved successfully alert once I know that to be true // for now just disable the button after a successful save @@ -145,7 +145,7 @@ struct TelemetryConfig: View { let expiration = node.sessionExpiration ?? Date() if expiration < Date() || node.telemetryConfig == nil { Logger.mesh.info("⚙️ Empty or expired telemetry module config requesting via PKI admin") - _ = bleManager.requestTelemetryModuleConfig(fromUser: connectedNode.user!, toUser: node.user!, adminIndex: connectedNode.myInfo?.adminIndex ?? 0) + _ = bleManager.requestTelemetryModuleConfig(fromUser: connectedNode.user!, toUser: node.user!) } } else { /// Legacy Administration diff --git a/Meshtastic/Views/Settings/Config/NetworkConfig.swift b/Meshtastic/Views/Settings/Config/NetworkConfig.swift index 57270f65..9d92ca03 100644 --- a/Meshtastic/Views/Settings/Config/NetworkConfig.swift +++ b/Meshtastic/Views/Settings/Config/NetworkConfig.swift @@ -114,7 +114,7 @@ struct NetworkConfig: View { network.enabledProtocols = self.udpEnabled ? UInt32(Config.NetworkConfig.ProtocolFlags.udpBroadcast.rawValue) : UInt32(Config.NetworkConfig.ProtocolFlags.noBroadcast.rawValue) // network.addressMode = Config.NetworkConfig.AddressMode.dhcp - let adminMessageId = bleManager.saveNetworkConfig(config: network, fromUser: connectedNode!.user!, toUser: node!.user!, adminIndex: connectedNode?.myInfo?.adminIndex ?? 0) + let adminMessageId = bleManager.saveNetworkConfig(config: network, fromUser: connectedNode!.user!, toUser: node!.user!) if adminMessageId > 0 { // Should show a saved successfully alert once I know that to be true // for now just disable the button after a successful save @@ -140,7 +140,7 @@ struct NetworkConfig: View { Logger.mesh.info("empty network config") let connectedNode = getNodeInfo(id: bleManager.connectedPeripheral.num, context: context) if node != nil && connectedNode != nil { - _ = bleManager.requestNetworkConfig(fromUser: connectedNode!.user!, toUser: node!.user!, adminIndex: connectedNode?.myInfo?.adminIndex ?? 0) + _ = bleManager.requestNetworkConfig(fromUser: connectedNode!.user!, toUser: node!.user!) } } } @@ -155,7 +155,7 @@ struct NetworkConfig: View { let expiration = node.sessionExpiration ?? Date() if expiration < Date() || node.networkConfig == nil { Logger.mesh.info("⚙️ Empty or expired network config requesting via PKI admin") - _ = bleManager.requestNetworkConfig(fromUser: connectedNode.user!, toUser: node.user!, adminIndex: connectedNode.myInfo?.adminIndex ?? 0) + _ = bleManager.requestNetworkConfig(fromUser: connectedNode.user!, toUser: node.user!) } } else { /// Legacy Administration diff --git a/Meshtastic/Views/Settings/Config/PositionConfig.swift b/Meshtastic/Views/Settings/Config/PositionConfig.swift index 3af2546d..537c1f26 100644 --- a/Meshtastic/Views/Settings/Config/PositionConfig.swift +++ b/Meshtastic/Views/Settings/Config/PositionConfig.swift @@ -345,7 +345,7 @@ struct PositionConfig: View { if includeSpeed { pf.insert(.Speed) } if includeHeading { pf.insert(.Heading) } pc.positionFlags = UInt32(pf.rawValue) - let adminMessageId = bleManager.savePositionConfig(config: pc, fromUser: connectedNode!.user!, toUser: node!.user!, adminIndex: connectedNode?.myInfo?.adminIndex ?? 0) + let adminMessageId = bleManager.savePositionConfig(config: pc, fromUser: connectedNode!.user!, toUser: node!.user!) if adminMessageId > 0 { // Disable the button after a successful save hasChanges = false @@ -412,7 +412,7 @@ struct PositionConfig: View { let expiration = node.sessionExpiration ?? Date() if expiration < Date() || node.positionConfig == nil { Logger.mesh.info("⚙️ Empty or expired position config requesting via PKI admin") - _ = bleManager.requestPositionConfig(fromUser: connectedNode.user!, toUser: node.user!, adminIndex: connectedNode.myInfo?.adminIndex ?? 0) + _ = bleManager.requestPositionConfig(fromUser: connectedNode.user!, toUser: node.user!) } } else { /// Legacy Administration diff --git a/Meshtastic/Views/Settings/Config/PowerConfig.swift b/Meshtastic/Views/Settings/Config/PowerConfig.swift index 9ba385ff..6087fec6 100644 --- a/Meshtastic/Views/Settings/Config/PowerConfig.swift +++ b/Meshtastic/Views/Settings/Config/PowerConfig.swift @@ -139,7 +139,7 @@ struct PowerConfig: View { let expiration = node.sessionExpiration ?? Date() if expiration < Date() || node.powerConfig == nil { Logger.mesh.info("⚙️ Empty or expired power config requesting via PKI admin") - _ = bleManager.requestPowerConfig(fromUser: connectedNode.user!, toUser: node.user!, adminIndex: connectedNode.myInfo?.adminIndex ?? 0) + _ = bleManager.requestPowerConfig(fromUser: connectedNode.user!, toUser: node.user!) } } else { /// Legacy Administration @@ -194,8 +194,7 @@ struct PowerConfig: View { let adminMessageId = bleManager.savePowerConfig( config: config, fromUser: fromUser, - toUser: toUser, - adminIndex: connectedNode.myInfo?.adminIndex ?? 0 + toUser: toUser ) if adminMessageId > 0 { // Should show a saved successfully alert once I know that to be true diff --git a/Meshtastic/Views/Settings/Config/SecurityConfig.swift b/Meshtastic/Views/Settings/Config/SecurityConfig.swift index 13f40f98..b2549f8d 100644 --- a/Meshtastic/Views/Settings/Config/SecurityConfig.swift +++ b/Meshtastic/Views/Settings/Config/SecurityConfig.swift @@ -65,6 +65,21 @@ struct SecurityConfig: View { Text("Used to create a shared key with a remote device.") .foregroundStyle(.secondary) .font(idiom == .phone ? .caption : .callout) + HStack(alignment: .firstTextBaseline) { + Label("Regenerate Private Key", systemImage: "arrow.clockwise.circle") + Spacer() + Button { + if let keyBytes = generatePrivateKey(count: 32) { + privateKey = keyBytes.base64EncodedString() + } + } label: { + Image(systemName: "lock.rotation") + .font(.title) + } + .buttonStyle(.bordered) + .buttonBorderShape(.capsule) + .controlSize(.small) + } Divider() Label("Primary Admin Key", systemImage: "key.viewfinder") SecureInput("Primary Admin Key", text: $adminKey, isValid: $hasValidAdminKey) @@ -199,7 +214,7 @@ struct SecurityConfig: View { let expiration = node.sessionExpiration ?? Date() if expiration < Date() || node.securityConfig == nil { Logger.mesh.info("⚙️ Empty or expired security config requesting via PKI admin") - _ = bleManager.requestSecurityConfig(fromUser: connectedNode.user!, toUser: node.user!, adminIndex: connectedNode.myInfo?.adminIndex ?? 0) + _ = bleManager.requestSecurityConfig(fromUser: connectedNode.user!, toUser: node.user!) } } else { if node.deviceConfig == nil { @@ -233,16 +248,25 @@ struct SecurityConfig: View { config.debugLogApiEnabled = debugLogApiEnabled config.adminChannelEnabled = adminChannelEnabled + let reboot = node?.securityConfig?.privateKey?.base64EncodedString() ?? "" != privateKey + let adminMessageId = bleManager.saveSecurityConfig( config: config, fromUser: fromUser, - toUser: toUser, - adminIndex: connectedNode.myInfo?.adminIndex ?? 0 + toUser: toUser ) if adminMessageId > 0 { // Should show a saved successfully alert once I know that to be true // for now just disable the button after a successful save hasChanges = false + if reboot { + if !bleManager.sendReboot( + fromUser: fromUser, + toUser: toUser + ) { + Logger.mesh.warning("Reboot Failed") + } + } goBack() } } @@ -260,4 +284,22 @@ struct SecurityConfig: View { self.adminChannelEnabled = node?.securityConfig?.adminChannelEnabled ?? false self.hasChanges = false } + + func generatePrivateKey(count: Int) -> Data? { + var randomBytes = Data(count: count) + let status = randomBytes.withUnsafeMutableBytes { (mutableBytes: UnsafeMutableRawBufferPointer) -> Int32 in + guard let pointer = mutableBytes.baseAddress?.assumingMemoryBound(to: UInt8.self) else { + return -1 // Indicate an error + } + return SecRandomCopyBytes(kSecRandomDefault, count, pointer) + } + + if status == errSecSuccess { + return randomBytes + } else { + // Handle error, perhaps by logging or throwing an exception + print("Error generating random bytes: \(status)") + return nil + } + } } diff --git a/Meshtastic/Views/Settings/Firmware.swift b/Meshtastic/Views/Settings/Firmware.swift index 2380b677..f8225e73 100644 --- a/Meshtastic/Views/Settings/Firmware.swift +++ b/Meshtastic/Views/Settings/Firmware.swift @@ -160,7 +160,7 @@ struct Firmware: View { Button { let connectedNode = getNodeInfo(id: bleManager.connectedPeripheral?.num ?? 0, context: context) if connectedNode != nil { - if !bleManager.sendRebootOta(fromUser: connectedNode!.user!, toUser: node!.user!, adminIndex: connectedNode!.myInfo!.adminIndex) { + if !bleManager.sendRebootOta(fromUser: connectedNode!.user!, toUser: node!.user!) { Logger.mesh.error("Reboot Failed") } } diff --git a/Meshtastic/Views/Settings/Settings.swift b/Meshtastic/Views/Settings/Settings.swift index 2dec1530..a4b664b9 100644 --- a/Meshtastic/Views/Settings/Settings.swift +++ b/Meshtastic/Views/Settings/Settings.swift @@ -432,7 +432,7 @@ struct Settings: View { let connectedNode = nodes.first(where: { $0.num == preferredNodeNum }) preferredNodeNum = Int(connectedNode?.num ?? 0)// Int(bleManager.connectedPeripheral != nil ? bleManager.connectedPeripheral?.num ?? 0 : 0) if connectedNode != nil && connectedNode?.user != nil && connectedNode?.myInfo != nil && node?.user != nil {// && node?.metadata == nil { - let adminMessageId = bleManager.requestDeviceMetadata(fromUser: connectedNode!.user!, toUser: node!.user!, adminIndex: connectedNode!.myInfo!.adminIndex, context: context) + let adminMessageId = bleManager.requestDeviceMetadata(fromUser: connectedNode!.user!, toUser: node!.user!, context: context) if adminMessageId > 0 { Logger.mesh.info("Sent node metadata request from node details") } diff --git a/Meshtastic/Views/Settings/UserConfig.swift b/Meshtastic/Views/Settings/UserConfig.swift index 708584a4..d281dc86 100644 --- a/Meshtastic/Views/Settings/UserConfig.swift +++ b/Meshtastic/Views/Settings/UserConfig.swift @@ -176,7 +176,7 @@ struct UserConfig: View { u.shortName = shortName u.longName = longName u.isUnmessagable = isUnmessagable - let adminMessageId = bleManager.saveUser(config: u, fromUser: connectedUser, toUser: node!.user!, adminIndex: connectedNode?.myInfo?.adminIndex ?? 0) + let adminMessageId = bleManager.saveUser(config: u, fromUser: connectedUser, toUser: node!.user!) if adminMessageId > 0 { hasChanges = false goBack() @@ -188,7 +188,7 @@ struct UserConfig: View { ham.callSign = longName ham.txPower = Int32(txPower) ham.frequency = overrideFrequency - let adminMessageId = bleManager.saveLicensedUser(ham: ham, fromUser: connectedUser, toUser: node!.user!, adminIndex: connectedNode?.myInfo?.adminIndex ?? 0) + let adminMessageId = bleManager.saveLicensedUser(ham: ham, fromUser: connectedUser, toUser: node!.user!) if adminMessageId > 0 { hasChanges = false goBack() From dd31989135106b242a7c2272788368a1d77d437c Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Sat, 14 Jun 2025 15:01:26 -0700 Subject: [PATCH 114/213] Tidy up private key regeneration --- Localizable.xcstrings | 56 ------------------- .../Settings/Config/SecurityConfig.swift | 25 +++------ 2 files changed, 7 insertions(+), 74 deletions(-) diff --git a/Localizable.xcstrings b/Localizable.xcstrings index eaf40011..61913134 100644 --- a/Localizable.xcstrings +++ b/Localizable.xcstrings @@ -2809,34 +2809,6 @@ } } }, - "Allow incoming device control over the insecure legacy admin channel." : { - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Erlaubt die eingehende Gerätesteuerung über den unsicheren Legacy-Admin-Kanal." - } - }, - "it" : { - "stringUnit" : { - "state" : "translated", - "value" : "Consentire il controllo del dispositivo in entrata attraverso il canale di amministrazione legacy non sicuro." - } - }, - "sr" : { - "stringUnit" : { - "state" : "translated", - "value" : "Дозволите контролу долазног уређаја над небезбедним старим администраторским каналом." - } - }, - "zh-Hant-TW" : { - "stringUnit" : { - "state" : "translated", - "value" : "允許經由不安全的傳統管理通道接收裝置控制指令。" - } - } - } - }, "Allow Position Requests" : { "localizations" : { "it" : { @@ -15740,34 +15712,6 @@ } } }, - "Legacy Administration" : { - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Alte Administrationsart" - } - }, - "it" : { - "stringUnit" : { - "state" : "translated", - "value" : "Amministrazione del patrimonio" - } - }, - "sr" : { - "stringUnit" : { - "state" : "translated", - "value" : "Стари начин администрације" - } - }, - "zh-Hant-TW" : { - "stringUnit" : { - "state" : "translated", - "value" : "舊版遠端管理" - } - } - } - }, "Level" : { "localizations" : { "de" : { diff --git a/Meshtastic/Views/Settings/Config/SecurityConfig.swift b/Meshtastic/Views/Settings/Config/SecurityConfig.swift index b2549f8d..84e785de 100644 --- a/Meshtastic/Views/Settings/Config/SecurityConfig.swift +++ b/Meshtastic/Views/Settings/Config/SecurityConfig.swift @@ -33,7 +33,6 @@ struct SecurityConfig: View { @State var isManaged = false @State var serialEnabled = false @State var debugLogApiEnabled = false - @State var adminChannelEnabled = false var body: some View { VStack { @@ -125,18 +124,13 @@ struct SecurityConfig: View { .toggleStyle(SwitchToggleStyle(tint: .accentColor)) } Section(header: Text("Administration")) { - if adminKey.length > 0 || adminChannelEnabled { + if adminKey.length > 0 || UserDefaults.enableAdministration { Toggle(isOn: $isManaged) { Label("Managed Device", systemImage: "gearshape.arrow.triangle.2.circlepath") Text("Device is managed by a mesh administrator, the user is unable to access any of the device settings.") } .toggleStyle(SwitchToggleStyle(tint: .accentColor)) } - Toggle(isOn: $adminChannelEnabled) { - Label("Legacy Administration", systemImage: "lock.slash") - Text("Allow incoming device control over the insecure legacy admin channel.") - } - .toggleStyle(SwitchToggleStyle(tint: .accentColor)) } } } @@ -158,17 +152,14 @@ struct SecurityConfig: View { .onChange(of: debugLogApiEnabled) { _, newDebugLogApiEnabled in if newDebugLogApiEnabled != node?.securityConfig?.debugLogApiEnabled { hasChanges = true } } - .onChange(of: adminChannelEnabled) { _, newAdminChannelEnabled in - if newAdminChannelEnabled != node?.securityConfig?.adminChannelEnabled { hasChanges = true } - } - .onChange(of: privateKey) { + .onChange(of: privateKey) { _, key in let tempKey = Data(base64Encoded: privateKey) ?? Data() if tempKey.count == 32 { hasValidPrivateKey = true } else { hasValidPrivateKey = false } - hasChanges = true + if key != node?.securityConfig?.privateKey?.base64EncodedString() ?? "" && hasValidPrivateKey { hasChanges = true } } .onChange(of: adminKey) { _, key in let tempKey = Data(base64Encoded: key) ?? Data() @@ -179,7 +170,7 @@ struct SecurityConfig: View { } else { hasValidAdminKey = false } - hasChanges = true + if key != node?.securityConfig?.adminKey?.base64EncodedString() ?? "" && hasValidAdminKey { hasChanges = true } } .onChange(of: adminKey2) { _, key in let tempKey = Data(base64Encoded: key) ?? Data() @@ -190,7 +181,7 @@ struct SecurityConfig: View { } else { hasValidAdminKey2 = false } - hasChanges = true + if key != node?.securityConfig?.adminKey2?.base64EncodedString() ?? "" && hasValidAdminKey2 { hasChanges = true } } .onChange(of: adminKey3) { _, key in let tempKey = Data(base64Encoded: key) ?? Data() @@ -201,10 +192,10 @@ struct SecurityConfig: View { } else { hasValidAdminKey3 = false } - hasChanges = true + if key != node?.securityConfig?.adminKey2?.base64EncodedString() ?? "" && hasValidAdminKey3 { hasChanges = true } } .onFirstAppear { - // Need to request a DeviceConfig from the remote node before allowing changes + // Need to request a SecurityConfig from the remote node before allowing changes if let connectedPeripheral = bleManager.connectedPeripheral, let node { let connectedNode = getNodeInfo(id: connectedPeripheral.num, context: context) if let connectedNode { @@ -246,7 +237,6 @@ struct SecurityConfig: View { config.isManaged = isManaged config.serialEnabled = serialEnabled config.debugLogApiEnabled = debugLogApiEnabled - config.adminChannelEnabled = adminChannelEnabled let reboot = node?.securityConfig?.privateKey?.base64EncodedString() ?? "" != privateKey @@ -281,7 +271,6 @@ struct SecurityConfig: View { self.isManaged = node?.securityConfig?.isManaged ?? false self.serialEnabled = node?.securityConfig?.serialEnabled ?? false self.debugLogApiEnabled = node?.securityConfig?.debugLogApiEnabled ?? false - self.adminChannelEnabled = node?.securityConfig?.adminChannelEnabled ?? false self.hasChanges = false } From 5746d2df7a9dff5c87ddd4cabe2f0bb6e376d3f6 Mon Sep 17 00:00:00 2001 From: Benjamin Faershtein <119711889+RCGV1@users.noreply.github.com> Date: Sat, 14 Jun 2025 15:13:18 -0700 Subject: [PATCH 115/213] Added retry timeout --- Meshtastic/Helpers/BLEManager.swift | 122 ++++++++++++++++++++++------ 1 file changed, 96 insertions(+), 26 deletions(-) diff --git a/Meshtastic/Helpers/BLEManager.swift b/Meshtastic/Helpers/BLEManager.swift index 4c093547..b93d0264 100644 --- a/Meshtastic/Helpers/BLEManager.swift +++ b/Meshtastic/Helpers/BLEManager.swift @@ -55,6 +55,13 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate let NONCE_ONLY_CONFIG = 69420 let NONCE_ONLY_DB = 69421 + private var isWaitingForWantConfigResponse = false + + private var wantConfigTimer: Timer? + private var wantConfigRetryCount = 0 + private let maxWantConfigRetries = 3 + private let wantConfigTimeoutInterval: TimeInterval = 5.0 + // MARK: init private override init() { @@ -499,35 +506,96 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate } return success } + + func sendWantConfig() { + isWaitingForWantConfigResponse = true + + guard connectedPeripheral?.peripheral.state ?? CBPeripheralState.disconnected == CBPeripheralState.connected else { return } + + if FROMRADIO_characteristic == nil { + Logger.mesh.error("🚨 \("Unsupported Firmware Version Detected, unable to connect to device.".localized, privacy: .public)") + invalidVersion = true + return + } else { + + let nodeName = connectedPeripheral?.peripheral.name ?? "Unknown".localized + let logString = String.localizedStringWithFormat("Issuing Want Config to %@".localized, nodeName) + Logger.mesh.info("🛎️ \(logString, privacy: .public)") + + // BLE Characteristics discovered, issue wantConfig + var toRadio: ToRadio = ToRadio() + configNonce = UInt32(NONCE_ONLY_DB) + if !isSubscribed { + configNonce = UInt32(NONCE_ONLY_CONFIG) // Get config first + } + toRadio.wantConfigID = configNonce + guard let binaryData: Data = try? toRadio.serializedData() else { + return + } + connectedPeripheral!.peripheral.writeValue(binaryData, for: TORADIO_characteristic, type: .withResponse) + + // Either Read the config complete value or from num notify value + guard connectedPeripheral != nil else { return } + connectedPeripheral!.peripheral.readValue(for: FROMRADIO_characteristic) + + // Start timeout timer + startWantConfigTimeout() + } + } - func sendWantConfig() { - guard connectedPeripheral?.peripheral.state ?? CBPeripheralState.disconnected == CBPeripheralState.connected else { return } + private func startWantConfigTimeout() { + // Cancel any existing timer + wantConfigTimer?.invalidate() + + // Start new timer + wantConfigTimer = Timer.scheduledTimer(withTimeInterval: wantConfigTimeoutInterval, repeats: false) { [weak self] _ in + self?.handleWantConfigTimeout() + } + } - if FROMRADIO_characteristic == nil { - Logger.mesh.error("🚨 \("Unsupported Firmware Version Detected, unable to connect to device.".localized, privacy: .public)") - invalidVersion = true - return - } else { + private func handleWantConfigTimeout() { + guard isWaitingForWantConfigResponse else { return } + + wantConfigRetryCount += 1 + + if wantConfigRetryCount < maxWantConfigRetries { + Logger.mesh.warning("⏰ Want Config timeout, retrying... (attempt \(self.wantConfigRetryCount + 1)/\(self.maxWantConfigRetries))") + sendWantConfig() + } else { + Logger.mesh.error("🚨 Want Config failed after \(self.maxWantConfigRetries) attempts, forcing disconnect") + forceDisconnect() + } + } - let nodeName = connectedPeripheral?.peripheral.name ?? "Unknown".localized - let logString = String.localizedStringWithFormat("Issuing Want Config to %@".localized, nodeName) - Logger.mesh.info("🛎️ \(logString, privacy: .public)") - // BLE Characteristics discovered, issue wantConfig - var toRadio: ToRadio = ToRadio() - configNonce = UInt32(NONCE_ONLY_DB) - if !isSubscribed { - configNonce = UInt32(NONCE_ONLY_CONFIG) // Get config first - } - toRadio.wantConfigID = configNonce - guard let binaryData: Data = try? toRadio.serializedData() else { - return - } - connectedPeripheral!.peripheral.writeValue(binaryData, for: TORADIO_characteristic, type: .withResponse) - // Either Read the config complete value or from num notify value - guard connectedPeripheral != nil else { return } - connectedPeripheral!.peripheral.readValue(for: FROMRADIO_characteristic) - } - } + func onWantConfigResponseReceived() { + if isWaitingForWantConfigResponse { + isWaitingForWantConfigResponse = false + wantConfigTimer?.invalidate() + wantConfigTimer = nil + wantConfigRetryCount = 0 // Reset retry count on success + } + } + + private func forceDisconnect() { + isWaitingForWantConfigResponse = false + wantConfigTimer?.invalidate() + wantConfigTimer = nil + wantConfigRetryCount = 0 + + disconnectPeripheral(reconnect: false) + + lastConnectionError = "Want Config timeout" + + Logger.mesh.info("🔌 Forced disconnect due to Want Config timeout") + } + + // Call this to reset the retry mechanism (e.g., on new connection) + func resetWantConfigRetries() { + wantConfigRetryCount = 0 + wantConfigTimer?.invalidate() + wantConfigTimer = nil + isWaitingForWantConfigResponse = false + } func peripheral(_ peripheral: CBPeripheral, didUpdateNotificationStateFor characteristic: CBCharacteristic, error: Error?) { if let error { @@ -700,6 +768,7 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate } // NodeInfo if decodedInfo.nodeInfo.num > 0 { + onWantConfigResponseReceived() nowKnown = true if let nodeInfo = nodeInfoPacket(nodeInfo: decodedInfo.nodeInfo, channel: decodedInfo.packet.channel, context: context) { if self.connectedPeripheral != nil && self.connectedPeripheral.num == nodeInfo.num { @@ -722,6 +791,7 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate } // Module Config if decodedInfo.moduleConfig.isInitialized && !invalidVersion && self.connectedPeripheral?.num != 0 { + onWantConfigResponseReceived() nowKnown = true moduleConfig(config: decodedInfo.moduleConfig, context: context, nodeNum: Int64(truncatingIfNeeded: self.connectedPeripheral?.num ?? 0), nodeLongName: self.connectedPeripheral.longName) if decodedInfo.moduleConfig.payloadVariant == ModuleConfig.OneOf_PayloadVariant.cannedMessage(decodedInfo.moduleConfig.cannedMessage) { From 647bb690f0065dd31d361bdc4e4f28dc6b046f08 Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Sat, 14 Jun 2025 18:43:14 -0700 Subject: [PATCH 116/213] Send key validation error to security config --- Meshtastic/Helpers/BLEManager.swift | 6 +++++- Meshtastic/Views/Settings/Config/SecurityConfig.swift | 2 +- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/Meshtastic/Helpers/BLEManager.swift b/Meshtastic/Helpers/BLEManager.swift index 5b417ff1..7bb256ee 100644 --- a/Meshtastic/Helpers/BLEManager.swift +++ b/Meshtastic/Helpers/BLEManager.swift @@ -713,6 +713,8 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate let message = CocoaMQTTMessage(topic: decodedInfo.mqttClientProxyMessage.topic, payload: [UInt8](decodedInfo.mqttClientProxyMessage.data), retained: decodedInfo.mqttClientProxyMessage.retained) mqttManager.mqttClientProxy?.publish(message) } else if decodedInfo.payloadVariant == FromRadio.OneOf_PayloadVariant.clientNotification(decodedInfo.clientNotification) { + + var path = "meshtastic:///settings/debugLogs" if decodedInfo.clientNotification.hasReplyID { /// Set Sent bool on TraceRouteEntity to false if we got rate limited if decodedInfo.clientNotification.message.starts(with: "TraceRoute") { @@ -726,6 +728,8 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate let nsError = error as NSError Logger.data.error("💥 [TraceRouteEntity] Error Updating Core Data: \(nsError, privacy: .public)") } + } else if decodedInfo.clientNotification.message.starts(with: "You Device is configured with a low entropy") || decodedInfo.clientNotification.message.starts(with: "Compromised keys detected") { + path = "meshtastic:///settings/security" } } let manager = LocalNotificationManager() @@ -736,7 +740,7 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate subtitle: "\(decodedInfo.clientNotification.level)".capitalized, content: decodedInfo.clientNotification.message, target: "settings", - path: "meshtastic:///settings/debugLogs" + path: path ) ] manager.schedule() diff --git a/Meshtastic/Views/Settings/Config/SecurityConfig.swift b/Meshtastic/Views/Settings/Config/SecurityConfig.swift index 84e785de..41e1614c 100644 --- a/Meshtastic/Views/Settings/Config/SecurityConfig.swift +++ b/Meshtastic/Views/Settings/Config/SecurityConfig.swift @@ -123,8 +123,8 @@ struct SecurityConfig: View { } .toggleStyle(SwitchToggleStyle(tint: .accentColor)) } - Section(header: Text("Administration")) { if adminKey.length > 0 || UserDefaults.enableAdministration { + Section(header: Text("Administration")) { Toggle(isOn: $isManaged) { Label("Managed Device", systemImage: "gearshape.arrow.triangle.2.circlepath") Text("Device is managed by a mesh administrator, the user is unable to access any of the device settings.") From 801703db2617627a6f6c0ecd259bbe52eadddf0f Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Sat, 14 Jun 2025 19:55:54 -0700 Subject: [PATCH 117/213] Want config timing cleanup --- Meshtastic/Helpers/BLEManager.swift | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/Meshtastic/Helpers/BLEManager.swift b/Meshtastic/Helpers/BLEManager.swift index 7bb256ee..45e0649a 100644 --- a/Meshtastic/Helpers/BLEManager.swift +++ b/Meshtastic/Helpers/BLEManager.swift @@ -52,16 +52,15 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate let FROMNUM_UUID = CBUUID(string: "0xED9DA18C-A800-4F66-A670-AA7547E34453") let LEGACY_LOGRADIO_UUID = CBUUID(string: "0x6C6FD238-78FA-436B-AACF-15C5BE1EF2E2") let LOGRADIO_UUID = CBUUID(string: "0x5a3d6e49-06e6-4423-9944-e9de8cdf9547") - + let NONCE_ONLY_CONFIG = 69420 let NONCE_ONLY_DB = 69421 private var isWaitingForWantConfigResponse = false - + private var wantConfigTimer: Timer? private var wantConfigRetryCount = 0 - private let maxWantConfigRetries = 3 - private let wantConfigTimeoutInterval: TimeInterval = 5.0 - + private let maxWantConfigRetries = 5 + private let wantConfigTimeoutInterval: TimeInterval = 10.0 // MARK: init private override init() { @@ -194,6 +193,7 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate if self.mqttProxyConnected { self.mqttManager.mqttClientProxy?.disconnect() } + self.wantConfigTimer?.invalidate() self.automaticallyReconnect = reconnect self.centralManager?.cancelPeripheralConnection(connectedPeripheral.peripheral) self.FROMRADIO_characteristic = nil @@ -583,9 +583,9 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate disconnectPeripheral(reconnect: false) - lastConnectionError = "Want Config timeout" + lastConnectionError = "Bluetooth connection timeout, keep your node closer.".localized - Logger.mesh.info("🔌 Forced disconnect due to Want Config timeout") + Logger.mesh.error("💥 [BLE] Forced disconnect due to Want Config timeout") } // Call this to reset the retry mechanism (e.g., on new connection) From 5e36402fc97ba1cc6b76970bfc9e9420d545af62 Mon Sep 17 00:00:00 2001 From: Benjamin Faershtein <119711889+RCGV1@users.noreply.github.com> Date: Sun, 15 Jun 2025 09:46:17 -0700 Subject: [PATCH 118/213] Reset wantConfigRetries on disconnect --- Meshtastic/Helpers/BLEManager.swift | 1 + 1 file changed, 1 insertion(+) diff --git a/Meshtastic/Helpers/BLEManager.swift b/Meshtastic/Helpers/BLEManager.swift index b93d0264..17b982b9 100644 --- a/Meshtastic/Helpers/BLEManager.swift +++ b/Meshtastic/Helpers/BLEManager.swift @@ -255,6 +255,7 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate // Disconnect Peripheral Event func centralManager(_ central: CBCentralManager, didDisconnectPeripheral peripheral: CBPeripheral, error: Error?) { + resetWantConfigRetries() self.connectedPeripheral = nil self.isConnecting = false self.isConnected = false From 9f9313ffba098adabaad987d505c4d59acc0ea27 Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Sun, 15 Jun 2025 09:47:27 -0700 Subject: [PATCH 119/213] Safer create user function --- Localizable.xcstrings | 35 -- Meshtastic.xcodeproj/project.pbxproj | 4 +- .../CoreData/UserEntityExtension.swift | 48 +- Meshtastic/Helpers/BLEManager.swift | 4 +- Meshtastic/Helpers/MeshPackets.swift | 39 +- .../Meshtastic.xcdatamodeld/.xccurrentversion | 2 +- .../contents | 505 ++++++++++++++++++ Meshtastic/Persistence/UpdateCoreData.swift | 48 +- 8 files changed, 619 insertions(+), 66 deletions(-) create mode 100644 Meshtastic/Meshtastic.xcdatamodeld/MeshtasticDataModelV 53.xcdatamodel/contents diff --git a/Localizable.xcstrings b/Localizable.xcstrings index 64ca3924..ad7d063d 100644 --- a/Localizable.xcstrings +++ b/Localizable.xcstrings @@ -15885,41 +15885,6 @@ } } }, - "Location" : { - "extractionState" : "stale", - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Standort" - } - }, - "it" : { - "stringUnit" : { - "state" : "translated", - "value" : "Posizione" - } - }, - "sr" : { - "stringUnit" : { - "state" : "translated", - "value" : "Локација:" - } - }, - "zh-Hans" : { - "stringUnit" : { - "state" : "translated", - "value" : "位置" - } - }, - "zh-Hant-TW" : { - "stringUnit" : { - "state" : "translated", - "value" : "位置" - } - } - } - }, "Location:" : { "localizations" : { "de" : { diff --git a/Meshtastic.xcodeproj/project.pbxproj b/Meshtastic.xcodeproj/project.pbxproj index 14a1c9b1..445f7e9f 100644 --- a/Meshtastic.xcodeproj/project.pbxproj +++ b/Meshtastic.xcodeproj/project.pbxproj @@ -371,6 +371,7 @@ DD1BD0ED2C603C91008C0C70 /* CustomFormatters.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomFormatters.swift; sourceTree = ""; }; DD1BD0F12C61D3AD008C0C70 /* MeshtasticDataModelV 42.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = "MeshtasticDataModelV 42.xcdatamodel"; sourceTree = ""; }; DD1BD0F22C63C65E008C0C70 /* SecurityConfig.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SecurityConfig.swift; sourceTree = ""; }; + DD1BEF462DFF284C0090CE24 /* MeshtasticDataModelV 53.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = "MeshtasticDataModelV 53.xcdatamodel"; sourceTree = ""; }; DD1BF2F82776FE2E008C8D2F /* UserMessageList.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserMessageList.swift; sourceTree = ""; }; DD2160AE28C5552500C17253 /* MQTTConfig.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MQTTConfig.swift; sourceTree = ""; }; DD23A50E26FD1B4400D9B90C /* PeripheralModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PeripheralModel.swift; sourceTree = ""; }; @@ -2002,6 +2003,7 @@ DD3CC6BA28E366DF00FA9159 /* Meshtastic.xcdatamodeld */ = { isa = XCVersionGroup; children = ( + DD1BEF462DFF284C0090CE24 /* MeshtasticDataModelV 53.xcdatamodel */, DD0836AB2DE7C7CB00A3A973 /* MeshtasticDataModelV 52.xcdatamodel */, DD63CB4E2DD4FBEA00AFCAE2 /* MeshtasticDataModelV 51.xcdatamodel */, 233E99B32D84969500CC3A77 /* MeshtasticDataModelV 50.xcdatamodel */, @@ -2055,7 +2057,7 @@ DD5D0A9A2931AD6B00F7EA61 /* MeshtasticDataModelV2.xcdatamodel */, DD3CC6BB28E366DF00FA9159 /* MeshtasticDataModel.xcdatamodel */, ); - currentVersion = DD0836AB2DE7C7CB00A3A973 /* MeshtasticDataModelV 52.xcdatamodel */; + currentVersion = DD1BEF462DFF284C0090CE24 /* MeshtasticDataModelV 53.xcdatamodel */; name = Meshtastic.xcdatamodeld; path = Meshtastic/Meshtastic.xcdatamodeld; sourceTree = ""; diff --git a/Meshtastic/Extensions/CoreData/UserEntityExtension.swift b/Meshtastic/Extensions/CoreData/UserEntityExtension.swift index 4030ea6b..92f097b3 100644 --- a/Meshtastic/Extensions/CoreData/UserEntityExtension.swift +++ b/Meshtastic/Extensions/CoreData/UserEntityExtension.swift @@ -106,14 +106,44 @@ extension UserEntity { } } } -public func createUser(num: Int64, context: NSManagedObjectContext) -> UserEntity { - let newUser = UserEntity(context: context) - newUser.num = Int64(num) - let userId = String(format: "%2X", num) - newUser.userId = "!\(userId)" - let last4 = String(userId.suffix(4)) - newUser.longName = "Meshtastic \(last4)" - newUser.shortName = last4 - newUser.hwModel = "UNSET" + +public func createUser(num: Int64, context: NSManagedObjectContext) throws -> UserEntity { + // Validate Input + guard num >= 0 else { + throw CoreDataError.invalidInput(message: "User number cannot be negative.") + } + + var newUser: UserEntity! // Use an implicitly unwrapped optional, but ensure it's assigned + + context.performAndWait { + newUser = UserEntity(context: context) + newUser.num = num + + let userId = String(format: "%016llX", num) + newUser.userId = "!\(userId)" + + let last4 = String(userId.suffix(4)) + newUser.longName = "Meshtastic \(last4)" + newUser.shortName = last4 + newUser.hwModel = "UNSET" + } + return newUser } + +enum CoreDataError: Error, LocalizedError { + case invalidInput(message: String) + case saveFailed(message: String) + case entityCreationFailed(message: String) // In case UserEntity(context:) fails for some reason + + var errorDescription: String? { + switch self { + case .invalidInput(let message): + return "Core Data Input Error: \(message)" + case .saveFailed(let message): + return "Core Data Save Error: \(message)" + case .entityCreationFailed(let message): + return "Core Data Entity Creation Error: \(message)" + } + } +} diff --git a/Meshtastic/Helpers/BLEManager.swift b/Meshtastic/Helpers/BLEManager.swift index 45e0649a..b710c272 100644 --- a/Meshtastic/Helpers/BLEManager.swift +++ b/Meshtastic/Helpers/BLEManager.swift @@ -59,8 +59,8 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate private var wantConfigTimer: Timer? private var wantConfigRetryCount = 0 - private let maxWantConfigRetries = 5 - private let wantConfigTimeoutInterval: TimeInterval = 10.0 + private let maxWantConfigRetries = 3 + private let wantConfigTimeoutInterval: TimeInterval = 10.0 // MARK: init private override init() { diff --git a/Meshtastic/Helpers/MeshPackets.swift b/Meshtastic/Helpers/MeshPackets.swift index 1ea33a67..75a62864 100644 --- a/Meshtastic/Helpers/MeshPackets.swift +++ b/Meshtastic/Helpers/MeshPackets.swift @@ -327,8 +327,14 @@ func nodeInfoPacket (nodeInfo: NodeInfo, channel: UInt32, context: NSManagedObje }} newNode.user = newUser } else if nodeInfo.num > Constants.minimumNodeNum { - let newUser = createUser(num: Int64(nodeInfo.num), context: context) - newNode.user = newUser + do { + let newUser = try createUser(num: Int64(nodeInfo.num), context: context) + newNode.user = newUser + } catch CoreDataError.invalidInput(let message) { + Logger.data.error("Error Creating a new Core Data UserEntity (Invalid Input) from node number: \(nodeInfo.num, privacy: .public) Error: \(message, privacy: .public)") + } catch { + Logger.data.error("Error Creating a new Core Data UserEntity from node number: \(nodeInfo.num, privacy: .public) Error: \(error.localizedDescription, privacy: .public)") + } } if (nodeInfo.position.longitudeI != 0 && nodeInfo.position.latitudeI != 0) && (nodeInfo.position.latitudeI != 373346000 && nodeInfo.position.longitudeI != -1220090000) { @@ -417,9 +423,14 @@ func nodeInfoPacket (nodeInfo: NodeInfo, channel: UInt32, context: NSManagedObje } } else { if fetchedNode[0].user == nil && nodeInfo.num > Constants.minimumNodeNum { - - let newUser = createUser(num: Int64(nodeInfo.num), context: context) - fetchedNode[0].user = newUser + do { + let newUser = try createUser(num: Int64(nodeInfo.num), context: context) + fetchedNode[0].user = newUser + } catch CoreDataError.invalidInput(let message) { + Logger.data.error("Error Creating a new Core Data UserEntity on an existing node (Invalid Input) from node number: \(nodeInfo.num, privacy: .public) Error: \(message, privacy: .public)") + } catch { + Logger.data.error("Error Creating a new Core Data UserEntity on an existing node from node number: \(nodeInfo.num, privacy: .public) Error: \(error.localizedDescription, privacy: .public)") + } } } @@ -929,7 +940,14 @@ func textMessageAppPacket( // For S&F broadcast messages, treat as a channel message (not a DM) newMessage.toUser = nil } else { - newMessage.toUser = createUser(num: Int64(truncatingIfNeeded: packet.to), context: context) + do { + let newUser = try createUser(num: Int64(truncatingIfNeeded: packet.to), context: context) + newMessage.toUser = newUser + } catch CoreDataError.invalidInput(let message) { + Logger.data.error("Error Creating a new Core Data UserEntity (Invalid Input) from node number: \(packet.to, privacy: .public) Error: \(message, privacy: .public)") + } catch { + Logger.data.error("Error Creating a new Core Data UserEntity from node number: \(packet.to, privacy: .public) Error: \(error.localizedDescription, privacy: .public)") + } } } if fetchedUsers.first(where: { $0.num == packet.from }) != nil { @@ -959,7 +977,14 @@ func textMessageAppPacket( } } else { /// Make a new from user if they are unknown - newMessage.fromUser = createUser(num: Int64(truncatingIfNeeded: packet.from), context: context) + do { + let newUser = try createUser(num: Int64(truncatingIfNeeded: packet.from), context: context) + newMessage.fromUser = newUser + } catch CoreDataError.invalidInput(let message) { + Logger.data.error("Error Creating a new Core Data UserEntity (Invalid Input) from node number: \(packet.from, privacy: .public) Error: \(message, privacy: .public)") + } catch { + Logger.data.error("Error Creating a new Core Data UserEntity from node number: \(packet.from, privacy: .public) Error: \(error.localizedDescription, privacy: .public)") + } } if packet.rxTime > 0 { newMessage.fromUser?.userNode?.lastHeard = Date(timeIntervalSince1970: TimeInterval(Int64(packet.rxTime))) diff --git a/Meshtastic/Meshtastic.xcdatamodeld/.xccurrentversion b/Meshtastic/Meshtastic.xcdatamodeld/.xccurrentversion index 160ee4b2..057ab601 100644 --- a/Meshtastic/Meshtastic.xcdatamodeld/.xccurrentversion +++ b/Meshtastic/Meshtastic.xcdatamodeld/.xccurrentversion @@ -3,6 +3,6 @@ _XCCurrentVersionName - MeshtasticDataModelV 52.xcdatamodel + MeshtasticDataModelV 53.xcdatamodel diff --git a/Meshtastic/Meshtastic.xcdatamodeld/MeshtasticDataModelV 53.xcdatamodel/contents b/Meshtastic/Meshtastic.xcdatamodeld/MeshtasticDataModelV 53.xcdatamodel/contents new file mode 100644 index 00000000..8f1c7f57 --- /dev/null +++ b/Meshtastic/Meshtastic.xcdatamodeld/MeshtasticDataModelV 53.xcdatamodel/contents @@ -0,0 +1,505 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/Meshtastic/Persistence/UpdateCoreData.swift b/Meshtastic/Persistence/UpdateCoreData.swift index a286b08e..0812311e 100644 --- a/Meshtastic/Persistence/UpdateCoreData.swift +++ b/Meshtastic/Persistence/UpdateCoreData.swift @@ -170,8 +170,14 @@ func upsertNodeInfoPacket (packet: MeshPacket, context: NSManagedObjectContext) if newUserMessage.id.isEmpty { if packet.from > Constants.minimumNodeNum { - let newUser = createUser(num: Int64(packet.from), context: context) - newNode.user = newUser + do { + let newUser = try createUser(num: Int64(truncatingIfNeeded: packet.from), context: context) + newNode.user = newUser + } catch CoreDataError.invalidInput(let message) { + Logger.data.error("Error Creating a new Core Data UserEntity (Invalid Input) from node number: \(packet.from, privacy: .public) Error: \(message, privacy: .public)") + } catch { + Logger.data.error("Error Creating a new Core Data UserEntity from node number: \(packet.from, privacy: .public) Error: \(error.localizedDescription, privacy: .public)") + } } } else { @@ -225,17 +231,32 @@ func upsertNodeInfoPacket (packet: MeshPacket, context: NSManagedObjectContext) } } else { if packet.from > Constants.minimumNodeNum { - let newUser = createUser(num: Int64(packet.from), context: context) - if !packet.publicKey.isEmpty { - newNode.user?.pkiEncrypted = true - newNode.user?.publicKey = packet.publicKey + do { + let newUser = try createUser(num: Int64(truncatingIfNeeded: packet.from), context: context) + if !packet.publicKey.isEmpty { + newNode.user?.pkiEncrypted = true + newNode.user?.publicKey = packet.publicKey + } + newNode.user = newUser + } catch CoreDataError.invalidInput(let message) { + Logger.data.error("Error Creating a new Core Data UserEntity (Invalid Input) from node number: \(packet.from, privacy: .public) Error: \(message, privacy: .public)") + } catch { + Logger.data.error("Error Creating a new Core Data UserEntity from node number: \(packet.from, privacy: .public) Error: \(error.localizedDescription, privacy: .public)") } - newNode.user = newUser } } if newNode.user == nil && packet.from > Constants.minimumNodeNum { - newNode.user = createUser(num: Int64(packet.from), context: context) + do { + let newUser = try createUser(num: Int64(packet.from), context: context) + newNode.user = newUser + } catch CoreDataError.invalidInput(let message) { + Logger.data.error("Error Creating a new Core Data UserEntity (Invalid Input) from node number: \(packet.from, privacy: .public) Error: \(message, privacy: .public)") + return + } catch { + Logger.data.error("Error Creating a new Core Data UserEntity from node number: \(packet.from, privacy: .public) Error: \(error.localizedDescription, privacy: .public)") + return + } } let myInfoEntity = MyInfoEntity(context: context) @@ -317,9 +338,14 @@ func upsertNodeInfoPacket (packet: MeshPacket, context: NSManagedObjectContext) fetchedNode[0].hopsAway = Int32(packet.hopStart - packet.hopLimit) } if fetchedNode[0].user == nil { - let newUser = createUser(num: Int64(truncatingIfNeeded: packet.from), context: context) - fetchedNode[0].user? = newUser - + do { + let newUser = try createUser(num: Int64(truncatingIfNeeded: packet.from), context: context) + fetchedNode[0].user = newUser + } catch CoreDataError.invalidInput(let message) { + Logger.data.error("Error Creating a new Core Data UserEntity on an existing node (Invalid Input) from node number: \(packet.from, privacy: .public) Error: \(message, privacy: .public)") + } catch { + Logger.data.error("Error Creating a new Core Data UserEntity on an existing node from node number: \(packet.from, privacy: .public) Error: \(error.localizedDescription, privacy: .public)") + } } do { try context.save() From fd901ac4d0148e112223729b93daadc60a434d9b Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Sun, 15 Jun 2025 12:40:30 -0700 Subject: [PATCH 120/213] Fix admin key 3 typo --- Meshtastic/Views/Settings/Config/SecurityConfig.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Meshtastic/Views/Settings/Config/SecurityConfig.swift b/Meshtastic/Views/Settings/Config/SecurityConfig.swift index 41e1614c..2095ae9b 100644 --- a/Meshtastic/Views/Settings/Config/SecurityConfig.swift +++ b/Meshtastic/Views/Settings/Config/SecurityConfig.swift @@ -192,7 +192,7 @@ struct SecurityConfig: View { } else { hasValidAdminKey3 = false } - if key != node?.securityConfig?.adminKey2?.base64EncodedString() ?? "" && hasValidAdminKey3 { hasChanges = true } + if key != node?.securityConfig?.adminKey3?.base64EncodedString() ?? "" && hasValidAdminKey3 { hasChanges = true } } .onFirstAppear { // Need to request a SecurityConfig from the remote node before allowing changes From f9b63e7ba5dceb6fa89576ccbf275d2e898a6fe2 Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Sun, 15 Jun 2025 14:14:18 -0700 Subject: [PATCH 121/213] Rollback if second user create fails --- Meshtastic/Persistence/UpdateCoreData.swift | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/Meshtastic/Persistence/UpdateCoreData.swift b/Meshtastic/Persistence/UpdateCoreData.swift index 0812311e..7aba8899 100644 --- a/Meshtastic/Persistence/UpdateCoreData.swift +++ b/Meshtastic/Persistence/UpdateCoreData.swift @@ -245,16 +245,18 @@ func upsertNodeInfoPacket (packet: MeshPacket, context: NSManagedObjectContext) } } } - + // User is messed up and has failed to create at least once, if this fails bail out if newNode.user == nil && packet.from > Constants.minimumNodeNum { do { let newUser = try createUser(num: Int64(packet.from), context: context) newNode.user = newUser } catch CoreDataError.invalidInput(let message) { Logger.data.error("Error Creating a new Core Data UserEntity (Invalid Input) from node number: \(packet.from, privacy: .public) Error: \(message, privacy: .public)") + context.rollback() return } catch { Logger.data.error("Error Creating a new Core Data UserEntity from node number: \(packet.from, privacy: .public) Error: \(error.localizedDescription, privacy: .public)") + context.rollback() return } } From 32711b8c74280a7b44bdcbc4c0268a92409ac4a4 Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Sun, 15 Jun 2025 14:29:16 -0700 Subject: [PATCH 122/213] Updated device hardware file --- Meshtastic/Resources/DeviceHardware.json | 112 ++++++++++++++++++++++- 1 file changed, 109 insertions(+), 3 deletions(-) diff --git a/Meshtastic/Resources/DeviceHardware.json b/Meshtastic/Resources/DeviceHardware.json index bbe99f2a..4163ebaa 100644 --- a/Meshtastic/Resources/DeviceHardware.json +++ b/Meshtastic/Resources/DeviceHardware.json @@ -229,7 +229,8 @@ "images": [ "tlora-t3s3-epaper.svg" ], - "requiresDfu": true + "requiresDfu": true, + "hasInkHud": true }, { "hwModel": 17, @@ -604,7 +605,7 @@ "hwModelSlug": "HELTEC_WIRELESS_TRACKER", "platformioTarget": "tracksenger", "architecture": "esp32-s3", - "activelySupported": false, + "activelySupported": true, "supportLevel": 3, "displayName": "TrackSenger (small TFT)", "requiresDfu": true, @@ -626,7 +627,7 @@ "hwModelSlug": "HELTEC_WIRELESS_TRACKER", "platformioTarget": "tracksenger-oled", "architecture": "esp32-s3", - "activelySupported": false, + "activelySupported": true, "supportLevel": 3, "displayName": "TrackSenger (big OLED)", "partitionScheme": "8MB" @@ -872,5 +873,110 @@ "images": [ "thinknode_m2.svg" ] + }, + { + "hwModel": 94, + "hwModelSlug": "HELTEC_MESH_POCKET", + "platformioTarget": "heltec-mesh-pocket-10000", + "architecture": "nrf52840", + "activelySupported": true, + "supportLevel": 1, + "displayName": "Heltec MeshPocket", + "tags": [ + "Heltec" + ], + "images": [ + "heltec_mesh_pocket.svg" + ], + "requiresDfu": true, + "hasInkHud": true + }, + { + "hwModel": 95, + "hwModelSlug": "SEEED_SOLAR_NODE", + "platformioTarget": "seeed_solar_node", + "architecture": "nrf52840", + "activelySupported": true, + "supportLevel": 1, + "displayName": "Seeed SenseCAP Solar Node", + "tags": [ + "Seeed" + ], + "images": [ + "seeed_solar.svg" + ], + "requiresDfu": true + }, + { + "hwModel": 99, + "hwModelSlug": "SEEED_WIO_TRACKER_L1", + "platformioTarget": "seeed_wio_tracker_L1", + "architecture": "nrf52840", + "activelySupported": true, + "supportLevel": 1, + "displayName": "Seeed Wio Tracker L1", + "tags": [ + "Seeed" + ], + "images": [ + "wio_tracker_l1_case.svg" + ], + "requiresDfu": true + }, + { + "hwModel": 97, + "hwModelSlug": "CROWPANEL", + "platformioTarget": "elecrow-adv1-43-50-70-tft", + "architecture": "esp32-s3", + "activelySupported": true, + "supportLevel": 1, + "displayName": "Crowpanel Adv 4.3/5.0/7.0 TFT", + "tags": [ + "Elecrow" + ], + "requiresDfu": true, + "images": [ + "crowpanel_5_0.svg", + "crowpanel_7_0.svg" + ], + "partitionScheme": "16MB", + "hasMui": true + }, + { + "hwModel": 97, + "hwModelSlug": "CROWPANEL", + "platformioTarget": "elecrow-adv-24-28-tft", + "architecture": "esp32-s3", + "activelySupported": true, + "supportLevel": 1, + "displayName": "Crowpanel Adv 2.4/2.8 TFT", + "tags": [ + "Elecrow" + ], + "requiresDfu": true, + "images": [ + "crowpanel_2_4.svg", + "crowpanel_2_8.svg" + ], + "partitionScheme": "16MB", + "hasMui": true + }, + { + "hwModel": 97, + "hwModelSlug": "CROWPANEL", + "platformioTarget": "elecrow-adv-35-tft", + "architecture": "esp32-s3", + "activelySupported": true, + "supportLevel": 1, + "displayName": "Crowpanel Adv 3.5 TFT", + "tags": [ + "Elecrow" + ], + "requiresDfu": true, + "images": [ + "crowpanel_3_5.svg" + ], + "partitionScheme": "16MB", + "hasMui": true } ] From c1a9d3a7ba0396be56345d73eaf8ae50745bd224 Mon Sep 17 00:00:00 2001 From: Ben Meadors Date: Sun, 15 Jun 2025 16:36:30 -0500 Subject: [PATCH 123/213] Father's day protos --- .../Sources/meshtastic/admin.pb.swift | 121 ++++++++++++++++ .../Sources/meshtastic/config.pb.swift | 81 +++++++++++ .../Sources/meshtastic/device_ui.pb.swift | 8 ++ .../Sources/meshtastic/mesh.pb.swift | 136 ++++++++++++++++++ protobufs | 2 +- 5 files changed, 347 insertions(+), 1 deletion(-) diff --git a/MeshtasticProtobufs/Sources/meshtastic/admin.pb.swift b/MeshtasticProtobufs/Sources/meshtastic/admin.pb.swift index 188799b9..2b539ef6 100644 --- a/MeshtasticProtobufs/Sources/meshtastic/admin.pb.swift +++ b/MeshtasticProtobufs/Sources/meshtastic/admin.pb.swift @@ -292,6 +292,17 @@ public struct AdminMessage: @unchecked Sendable { set {payloadVariant = .removeBackupPreferences(newValue)} } + /// + /// Send an input event to the node. + /// This is used to trigger physical input events like button presses, touch events, etc. + public var sendInputEvent: AdminMessage.InputEvent { + get { + if case .sendInputEvent(let v)? = payloadVariant {return v} + return AdminMessage.InputEvent() + } + set {payloadVariant = .sendInputEvent(newValue)} + } + /// /// Set the owner for this node public var setOwner: User { @@ -663,6 +674,10 @@ public struct AdminMessage: @unchecked Sendable { /// Remove backups of the node's preferences case removeBackupPreferences(AdminMessage.BackupLocation) /// + /// Send an input event to the node. + /// This is used to trigger physical input events like button presses, touch events, etc. + case sendInputEvent(AdminMessage.InputEvent) + /// /// Set the owner for this node case setOwner(User) /// @@ -1014,6 +1029,34 @@ public struct AdminMessage: @unchecked Sendable { } + /// + /// Input event message to be sent to the node. + 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. + + /// + /// The input event code + public var eventCode: UInt32 = 0 + + /// + /// Keyboard character code + public var kbChar: UInt32 = 0 + + /// + /// The touch X coordinate + public var touchX: UInt32 = 0 + + /// + /// The touch Y coordinate + public var touchY: UInt32 = 0 + + public var unknownFields = SwiftProtobuf.UnknownStorage() + + public init() {} + } + public init() {} } @@ -1083,6 +1126,10 @@ public struct SharedContact: Sendable { /// Clears the value of `user`. Subsequent reads from it will return its default value. public mutating func clearUser() {self._user = nil} + /// + /// Add this contact to the blocked / ignored list + public var shouldIgnore: Bool = false + public var unknownFields = SwiftProtobuf.UnknownStorage() public init() {} @@ -1215,6 +1262,7 @@ extension AdminMessage: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementat 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"), @@ -1491,6 +1539,19 @@ extension AdminMessage: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementat self.payloadVariant = .removeBackupPreferences(v) } }() + case 27: try { + var v: AdminMessage.InputEvent? + var hadOneofValue = false + if let current = self.payloadVariant { + hadOneofValue = true + if case .sendInputEvent(let m) = current {v = m} + } + try decoder.decodeSingularMessageField(value: &v) + if let v = v { + if hadOneofValue {try decoder.handleConflictingOneOf()} + self.payloadVariant = .sendInputEvent(v) + } + }() case 32: try { var v: User? var hadOneofValue = false @@ -1872,6 +1933,10 @@ extension AdminMessage: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementat guard case .removeBackupPreferences(let v)? = self.payloadVariant else { preconditionFailure() } try visitor.visitSingularEnumField(value: v, fieldNumber: 26) }() + case .sendInputEvent?: try { + guard case .sendInputEvent(let v)? = self.payloadVariant else { preconditionFailure() } + try visitor.visitSingularMessageField(value: v, fieldNumber: 27) + }() case .setOwner?: try { guard case .setOwner(let v)? = self.payloadVariant else { preconditionFailure() } try visitor.visitSingularMessageField(value: v, fieldNumber: 32) @@ -2040,6 +2105,56 @@ extension AdminMessage.BackupLocation: SwiftProtobuf._ProtoNameProviding { ] } +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 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.decodeSingularUInt32Field(value: &self.eventCode) }() + case 2: try { try decoder.decodeSingularUInt32Field(value: &self.kbChar) }() + case 3: try { try decoder.decodeSingularUInt32Field(value: &self.touchX) }() + case 4: try { try decoder.decodeSingularUInt32Field(value: &self.touchY) }() + default: break + } + } + } + + public func traverse(visitor: inout V) throws { + if self.eventCode != 0 { + try visitor.visitSingularUInt32Field(value: self.eventCode, fieldNumber: 1) + } + if self.kbChar != 0 { + try visitor.visitSingularUInt32Field(value: self.kbChar, fieldNumber: 2) + } + if self.touchX != 0 { + try visitor.visitSingularUInt32Field(value: self.touchX, fieldNumber: 3) + } + if self.touchY != 0 { + try visitor.visitSingularUInt32Field(value: self.touchY, fieldNumber: 4) + } + try unknownFields.traverse(visitor: &visitor) + } + + public static func ==(lhs: AdminMessage.InputEvent, rhs: AdminMessage.InputEvent) -> Bool { + if lhs.eventCode != rhs.eventCode {return false} + if lhs.kbChar != rhs.kbChar {return false} + if lhs.touchX != rhs.touchX {return false} + if lhs.touchY != rhs.touchY {return false} + if lhs.unknownFields != rhs.unknownFields {return false} + return true + } +} + extension HamParameters: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { public static let protoMessageName: String = _protobuf_package + ".HamParameters" public static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ @@ -2127,6 +2242,7 @@ extension SharedContact: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementa public static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ 1: .standard(proto: "node_num"), 2: .same(proto: "user"), + 3: .standard(proto: "should_ignore"), ] public mutating func decodeMessage(decoder: inout D) throws { @@ -2137,6 +2253,7 @@ extension SharedContact: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementa switch fieldNumber { case 1: try { try decoder.decodeSingularUInt32Field(value: &self.nodeNum) }() case 2: try { try decoder.decodeSingularMessageField(value: &self._user) }() + case 3: try { try decoder.decodeSingularBoolField(value: &self.shouldIgnore) }() default: break } } @@ -2153,12 +2270,16 @@ extension SharedContact: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementa try { if let v = self._user { try visitor.visitSingularMessageField(value: v, fieldNumber: 2) } }() + if self.shouldIgnore != false { + try visitor.visitSingularBoolField(value: self.shouldIgnore, fieldNumber: 3) + } try unknownFields.traverse(visitor: &visitor) } public static func ==(lhs: SharedContact, rhs: SharedContact) -> Bool { if lhs.nodeNum != rhs.nodeNum {return false} if lhs._user != rhs._user {return false} + if lhs.shouldIgnore != rhs.shouldIgnore {return false} if lhs.unknownFields != rhs.unknownFields {return false} return true } diff --git a/MeshtasticProtobufs/Sources/meshtastic/config.pb.swift b/MeshtasticProtobufs/Sources/meshtastic/config.pb.swift index 12a57c69..e37bc908 100644 --- a/MeshtasticProtobufs/Sources/meshtastic/config.pb.swift +++ b/MeshtasticProtobufs/Sources/meshtastic/config.pb.swift @@ -189,6 +189,11 @@ public struct Config: Sendable { /// If true, disable the default blinking LED (LED_PIN) behavior on the device public var ledHeartbeatDisabled: Bool = false + /// + /// Controls buzzer behavior for audio feedback + /// Defaults to ENABLED + public var buzzerMode: Config.DeviceConfig.BuzzerMode = .allEnabled + public var unknownFields = SwiftProtobuf.UnknownStorage() /// @@ -406,6 +411,67 @@ public struct Config: Sendable { } + /// + /// Defines buzzer behavior for audio feedback + public enum BuzzerMode: SwiftProtobuf.Enum, Swift.CaseIterable { + public typealias RawValue = Int + + /// + /// Default behavior. + /// Buzzer is enabled for all audio feedback including button presses and alerts. + case allEnabled // = 0 + + /// + /// Disabled. + /// All buzzer audio feedback is disabled. + case disabled // = 1 + + /// + /// Notifications Only. + /// Buzzer is enabled only for notifications and alerts, but not for button presses. + /// External notification config determines the specifics of the notification behavior. + case notificationsOnly // = 2 + + /// + /// Non-notification system buzzer tones only. + /// Buzzer is enabled only for non-notification tones such as button presses, startup, shutdown, but not for alerts. + case systemOnly // = 3 + case UNRECOGNIZED(Int) + + public init() { + self = .allEnabled + } + + public init?(rawValue: Int) { + switch rawValue { + case 0: self = .allEnabled + case 1: self = .disabled + case 2: self = .notificationsOnly + case 3: self = .systemOnly + default: self = .UNRECOGNIZED(rawValue) + } + } + + public var rawValue: Int { + switch self { + case .allEnabled: return 0 + case .disabled: return 1 + case .notificationsOnly: return 2 + case .systemOnly: return 3 + case .UNRECOGNIZED(let i): return i + } + } + + // The compiler won't synthesize support with the UNRECOGNIZED case. + public static let allCases: [Config.DeviceConfig.BuzzerMode] = [ + .allEnabled, + .disabled, + .notificationsOnly, + .systemOnly, + ] + + } + public init() {} } @@ -2063,6 +2129,7 @@ extension Config.DeviceConfig: SwiftProtobuf.Message, SwiftProtobuf._MessageImpl 10: .standard(proto: "disable_triple_click"), 11: .same(proto: "tzdef"), 12: .standard(proto: "led_heartbeat_disabled"), + 13: .standard(proto: "buzzer_mode"), ] public mutating func decodeMessage(decoder: inout D) throws { @@ -2082,6 +2149,7 @@ extension Config.DeviceConfig: SwiftProtobuf.Message, SwiftProtobuf._MessageImpl case 10: try { try decoder.decodeSingularBoolField(value: &self.disableTripleClick) }() case 11: try { try decoder.decodeSingularStringField(value: &self.tzdef) }() case 12: try { try decoder.decodeSingularBoolField(value: &self.ledHeartbeatDisabled) }() + case 13: try { try decoder.decodeSingularEnumField(value: &self.buzzerMode) }() default: break } } @@ -2121,6 +2189,9 @@ extension Config.DeviceConfig: SwiftProtobuf.Message, SwiftProtobuf._MessageImpl if self.ledHeartbeatDisabled != false { try visitor.visitSingularBoolField(value: self.ledHeartbeatDisabled, fieldNumber: 12) } + if self.buzzerMode != .allEnabled { + try visitor.visitSingularEnumField(value: self.buzzerMode, fieldNumber: 13) + } try unknownFields.traverse(visitor: &visitor) } @@ -2136,6 +2207,7 @@ extension Config.DeviceConfig: SwiftProtobuf.Message, SwiftProtobuf._MessageImpl if lhs.disableTripleClick != rhs.disableTripleClick {return false} if lhs.tzdef != rhs.tzdef {return false} if lhs.ledHeartbeatDisabled != rhs.ledHeartbeatDisabled {return false} + if lhs.buzzerMode != rhs.buzzerMode {return false} if lhs.unknownFields != rhs.unknownFields {return false} return true } @@ -2169,6 +2241,15 @@ extension Config.DeviceConfig.RebroadcastMode: SwiftProtobuf._ProtoNameProviding ] } +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"), + ] +} + extension Config.PositionConfig: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { public static let protoMessageName: String = Config.protoMessageName + ".PositionConfig" public static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ diff --git a/MeshtasticProtobufs/Sources/meshtastic/device_ui.pb.swift b/MeshtasticProtobufs/Sources/meshtastic/device_ui.pb.swift index 637b20a8..9607abe1 100644 --- a/MeshtasticProtobufs/Sources/meshtastic/device_ui.pb.swift +++ b/MeshtasticProtobufs/Sources/meshtastic/device_ui.pb.swift @@ -141,6 +141,10 @@ public enum Language: SwiftProtobuf.Enum, Swift.CaseIterable { /// Ukrainian case ukrainian // = 16 + /// + /// Bulgarian + case bulgarian // = 17 + /// /// Simplified Chinese (experimental) case simplifiedChinese // = 30 @@ -173,6 +177,7 @@ public enum Language: SwiftProtobuf.Enum, Swift.CaseIterable { case 14: self = .norwegian case 15: self = .slovenian case 16: self = .ukrainian + case 17: self = .bulgarian case 30: self = .simplifiedChinese case 31: self = .traditionalChinese default: self = .UNRECOGNIZED(rawValue) @@ -198,6 +203,7 @@ public enum Language: SwiftProtobuf.Enum, Swift.CaseIterable { case .norwegian: return 14 case .slovenian: return 15 case .ukrainian: return 16 + case .bulgarian: return 17 case .simplifiedChinese: return 30 case .traditionalChinese: return 31 case .UNRECOGNIZED(let i): return i @@ -223,6 +229,7 @@ public enum Language: SwiftProtobuf.Enum, Swift.CaseIterable { .norwegian, .slovenian, .ukrainian, + .bulgarian, .simplifiedChinese, .traditionalChinese, ] @@ -502,6 +509,7 @@ extension Language: SwiftProtobuf._ProtoNameProviding { 14: .same(proto: "NORWEGIAN"), 15: .same(proto: "SLOVENIAN"), 16: .same(proto: "UKRAINIAN"), + 17: .same(proto: "BULGARIAN"), 30: .same(proto: "SIMPLIFIED_CHINESE"), 31: .same(proto: "TRADITIONAL_CHINESE"), ] diff --git a/MeshtasticProtobufs/Sources/meshtastic/mesh.pb.swift b/MeshtasticProtobufs/Sources/meshtastic/mesh.pb.swift index 407d395f..85981376 100644 --- a/MeshtasticProtobufs/Sources/meshtastic/mesh.pb.swift +++ b/MeshtasticProtobufs/Sources/meshtastic/mesh.pb.swift @@ -458,6 +458,18 @@ public enum HardwareModel: SwiftProtobuf.Enum, Swift.CaseIterable { /// Reserved ID for future and past use case qwantzTinyArms // = 101 + ///* + /// Lilygo T-Deck Pro + case tDeckPro // = 102 + + ///* + /// Lilygo TLora Pager + case tLoraPager // = 103 + + ///* + /// GAT562 Mesh Trial Tracker + case gat562MeshTrialTracker // = 104 + /// /// ------------------------------------------------------------------------------------------------------------------------------------------ /// 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. @@ -573,6 +585,9 @@ public enum HardwareModel: SwiftProtobuf.Enum, Swift.CaseIterable { case 99: self = .seeedWioTrackerL1 case 100: self = .seeedWioTrackerL1Eink case 101: self = .qwantzTinyArms + case 102: self = .tDeckPro + case 103: self = .tLoraPager + case 104: self = .gat562MeshTrialTracker case 255: self = .privateHw default: self = .UNRECOGNIZED(rawValue) } @@ -682,6 +697,9 @@ public enum HardwareModel: SwiftProtobuf.Enum, Swift.CaseIterable { case .seeedWioTrackerL1: return 99 case .seeedWioTrackerL1Eink: return 100 case .qwantzTinyArms: return 101 + case .tDeckPro: return 102 + case .tLoraPager: return 103 + case .gat562MeshTrialTracker: return 104 case .privateHw: return 255 case .UNRECOGNIZED(let i): return i } @@ -791,6 +809,9 @@ public enum HardwareModel: SwiftProtobuf.Enum, Swift.CaseIterable { .seeedWioTrackerL1, .seeedWioTrackerL1Eink, .qwantzTinyArms, + .tDeckPro, + .tLoraPager, + .gat562MeshTrialTracker, .privateHw, ] @@ -2991,12 +3012,30 @@ public struct ClientNotification: Sendable { set {payloadVariant = .keyVerificationFinal(newValue)} } + public var duplicatedPublicKey: DuplicatedPublicKey { + get { + if case .duplicatedPublicKey(let v)? = payloadVariant {return v} + return DuplicatedPublicKey() + } + set {payloadVariant = .duplicatedPublicKey(newValue)} + } + + public var lowEntropyKey: LowEntropyKey { + get { + if case .lowEntropyKey(let v)? = payloadVariant {return v} + return LowEntropyKey() + } + set {payloadVariant = .lowEntropyKey(newValue)} + } + public var unknownFields = SwiftProtobuf.UnknownStorage() public enum OneOf_PayloadVariant: Equatable, Sendable { case keyVerificationNumberInform(KeyVerificationNumberInform) case keyVerificationNumberRequest(KeyVerificationNumberRequest) case keyVerificationFinal(KeyVerificationFinal) + case duplicatedPublicKey(DuplicatedPublicKey) + case lowEntropyKey(LowEntropyKey) } @@ -3053,6 +3092,26 @@ public struct KeyVerificationFinal: Sendable { public init() {} } +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. + + public var unknownFields = SwiftProtobuf.UnknownStorage() + + public init() {} +} + +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. + + public var unknownFields = SwiftProtobuf.UnknownStorage() + + public init() {} +} + /// /// Individual File info for the device public struct FileInfo: Sendable { @@ -3578,6 +3637,9 @@ extension HardwareModel: SwiftProtobuf._ProtoNameProviding { 99: .same(proto: "SEEED_WIO_TRACKER_L1"), 100: .same(proto: "SEEED_WIO_TRACKER_L1_EINK"), 101: .same(proto: "QWANTZ_TINY_ARMS"), + 102: .same(proto: "T_DECK_PRO"), + 103: .same(proto: "T_LORA_PAGER"), + 104: .same(proto: "GAT562_MESH_TRIAL_TRACKER"), 255: .same(proto: "PRIVATE_HW"), ] } @@ -5348,6 +5410,8 @@ extension ClientNotification: SwiftProtobuf.Message, SwiftProtobuf._MessageImple 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 mutating func decodeMessage(decoder: inout D) throws { @@ -5399,6 +5463,32 @@ extension ClientNotification: SwiftProtobuf.Message, SwiftProtobuf._MessageImple self.payloadVariant = .keyVerificationFinal(v) } }() + case 14: try { + var v: DuplicatedPublicKey? + var hadOneofValue = false + if let current = self.payloadVariant { + hadOneofValue = true + if case .duplicatedPublicKey(let m) = current {v = m} + } + try decoder.decodeSingularMessageField(value: &v) + if let v = v { + if hadOneofValue {try decoder.handleConflictingOneOf()} + self.payloadVariant = .duplicatedPublicKey(v) + } + }() + case 15: try { + var v: LowEntropyKey? + var hadOneofValue = false + if let current = self.payloadVariant { + hadOneofValue = true + if case .lowEntropyKey(let m) = current {v = m} + } + try decoder.decodeSingularMessageField(value: &v) + if let v = v { + if hadOneofValue {try decoder.handleConflictingOneOf()} + self.payloadVariant = .lowEntropyKey(v) + } + }() default: break } } @@ -5434,6 +5524,14 @@ extension ClientNotification: SwiftProtobuf.Message, SwiftProtobuf._MessageImple guard case .keyVerificationFinal(let v)? = self.payloadVariant else { preconditionFailure() } try visitor.visitSingularMessageField(value: v, fieldNumber: 13) }() + case .duplicatedPublicKey?: try { + guard case .duplicatedPublicKey(let v)? = self.payloadVariant else { preconditionFailure() } + try visitor.visitSingularMessageField(value: v, fieldNumber: 14) + }() + case .lowEntropyKey?: try { + guard case .lowEntropyKey(let v)? = self.payloadVariant else { preconditionFailure() } + try visitor.visitSingularMessageField(value: v, fieldNumber: 15) + }() case nil: break } try unknownFields.traverse(visitor: &visitor) @@ -5582,6 +5680,44 @@ extension KeyVerificationFinal: SwiftProtobuf.Message, SwiftProtobuf._MessageImp } } +extension DuplicatedPublicKey: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { + public static let protoMessageName: String = _protobuf_package + ".DuplicatedPublicKey" + public static let _protobuf_nameMap = SwiftProtobuf._NameMap() + + public mutating func decodeMessage(decoder: inout D) throws { + // Load everything into unknown fields + while try decoder.nextFieldNumber() != nil {} + } + + public func traverse(visitor: inout V) throws { + try unknownFields.traverse(visitor: &visitor) + } + + public static func ==(lhs: DuplicatedPublicKey, rhs: DuplicatedPublicKey) -> Bool { + if lhs.unknownFields != rhs.unknownFields {return false} + return true + } +} + +extension LowEntropyKey: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { + public static let protoMessageName: String = _protobuf_package + ".LowEntropyKey" + public static let _protobuf_nameMap = SwiftProtobuf._NameMap() + + public mutating func decodeMessage(decoder: inout D) throws { + // Load everything into unknown fields + while try decoder.nextFieldNumber() != nil {} + } + + public func traverse(visitor: inout V) throws { + try unknownFields.traverse(visitor: &visitor) + } + + public static func ==(lhs: LowEntropyKey, rhs: LowEntropyKey) -> Bool { + if lhs.unknownFields != rhs.unknownFields {return false} + return true + } +} + extension FileInfo: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { public static let protoMessageName: String = _protobuf_package + ".FileInfo" public static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ diff --git a/protobufs b/protobufs index 816595c8..c758376d 160000 --- a/protobufs +++ b/protobufs @@ -1 +1 @@ -Subproject commit 816595c8bbdfc3b4388e11348ccd043294d58705 +Subproject commit c758376d04cf5d3d42de24f9388836a18bae9a76 From a4847c5c8ea5886d02b6d75a6d307580ea2cc73c Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Sun, 15 Jun 2025 19:23:43 -0700 Subject: [PATCH 124/213] Add SVG files for heltec mesh pocket and seeed solar node --- .../HELTECMESHPOCKET.imageset/Contents.json | 12 ++ .../heltec_mesh_pocket.svg | 196 ++++++++++++++++++ .../SEEEDSOLARNODE.imageset/Contents.json | 12 ++ .../SEEEDSOLARNODE.imageset/seeed_solar.svg | 1 + 4 files changed, 221 insertions(+) create mode 100644 Meshtastic/Assets.xcassets/HELTECMESHPOCKET.imageset/Contents.json create mode 100644 Meshtastic/Assets.xcassets/HELTECMESHPOCKET.imageset/heltec_mesh_pocket.svg create mode 100644 Meshtastic/Assets.xcassets/SEEEDSOLARNODE.imageset/Contents.json create mode 100644 Meshtastic/Assets.xcassets/SEEEDSOLARNODE.imageset/seeed_solar.svg diff --git a/Meshtastic/Assets.xcassets/HELTECMESHPOCKET.imageset/Contents.json b/Meshtastic/Assets.xcassets/HELTECMESHPOCKET.imageset/Contents.json new file mode 100644 index 00000000..4234e370 --- /dev/null +++ b/Meshtastic/Assets.xcassets/HELTECMESHPOCKET.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "heltec_mesh_pocket.svg", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Meshtastic/Assets.xcassets/HELTECMESHPOCKET.imageset/heltec_mesh_pocket.svg b/Meshtastic/Assets.xcassets/HELTECMESHPOCKET.imageset/heltec_mesh_pocket.svg new file mode 100644 index 00000000..1af4f5c6 --- /dev/null +++ b/Meshtastic/Assets.xcassets/HELTECMESHPOCKET.imageset/heltec_mesh_pocket.svg @@ -0,0 +1,196 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Meshtastic/Assets.xcassets/SEEEDSOLARNODE.imageset/Contents.json b/Meshtastic/Assets.xcassets/SEEEDSOLARNODE.imageset/Contents.json new file mode 100644 index 00000000..d1ac2a26 --- /dev/null +++ b/Meshtastic/Assets.xcassets/SEEEDSOLARNODE.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "seeed_solar.svg", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Meshtastic/Assets.xcassets/SEEEDSOLARNODE.imageset/seeed_solar.svg b/Meshtastic/Assets.xcassets/SEEEDSOLARNODE.imageset/seeed_solar.svg new file mode 100644 index 00000000..3f2b5d47 --- /dev/null +++ b/Meshtastic/Assets.xcassets/SEEEDSOLARNODE.imageset/seeed_solar.svg @@ -0,0 +1 @@ + \ No newline at end of file From 30de672389731e9b906eace06c3a798822a00aae Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Sun, 15 Jun 2025 19:46:45 -0700 Subject: [PATCH 125/213] Bump deployment target --- 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 445f7e9f..58b50820 100644 --- a/Meshtastic.xcodeproj/project.pbxproj +++ b/Meshtastic.xcodeproj/project.pbxproj @@ -1803,7 +1803,7 @@ INFOPLIST_FILE = Meshtastic/Info.plist; INFOPLIST_KEY_CFBundleDisplayName = Meshtastic; INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.utilities"; - IPHONEOS_DEPLOYMENT_TARGET = 17.0; + IPHONEOS_DEPLOYMENT_TARGET = 17.3; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -1836,7 +1836,7 @@ INFOPLIST_FILE = Meshtastic/Info.plist; INFOPLIST_KEY_CFBundleDisplayName = Meshtastic; INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.utilities"; - IPHONEOS_DEPLOYMENT_TARGET = 17.0; + IPHONEOS_DEPLOYMENT_TARGET = 17.3; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -1866,7 +1866,7 @@ INFOPLIST_FILE = Widgets/Info.plist; INFOPLIST_KEY_CFBundleDisplayName = Widgets; INFOPLIST_KEY_NSHumanReadableCopyright = ""; - IPHONEOS_DEPLOYMENT_TARGET = 17.0; + IPHONEOS_DEPLOYMENT_TARGET = 17.3; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -1898,7 +1898,7 @@ INFOPLIST_FILE = Widgets/Info.plist; INFOPLIST_KEY_CFBundleDisplayName = Widgets; INFOPLIST_KEY_NSHumanReadableCopyright = ""; - IPHONEOS_DEPLOYMENT_TARGET = 17.0; + IPHONEOS_DEPLOYMENT_TARGET = 17.3; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", From be263b412ad34c6a79494c96456f26b2ec41ee3f Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Sun, 15 Jun 2025 20:04:21 -0700 Subject: [PATCH 126/213] 12 hour clock display config value --- Localizable.xcstrings | 6 ++++++ .../MeshtasticDataModelV 53.xcdatamodel/contents | 1 + Meshtastic/Persistence/UpdateCoreData.swift | 2 ++ .../Views/Settings/Config/DisplayConfig.swift | 13 ++++++++++++- 4 files changed, 21 insertions(+), 1 deletion(-) diff --git a/Localizable.xcstrings b/Localizable.xcstrings index ad7d063d..d019ab3e 100644 --- a/Localizable.xcstrings +++ b/Localizable.xcstrings @@ -1512,6 +1512,9 @@ } } } + }, + "12 Hour Clock" : { + }, "25" : { "localizations" : { @@ -27693,6 +27696,9 @@ } } } + }, + "Sets the screen clock format to 12-hour." : { + }, "Settings" : { "localizations" : { diff --git a/Meshtastic/Meshtastic.xcdatamodeld/MeshtasticDataModelV 53.xcdatamodel/contents b/Meshtastic/Meshtastic.xcdatamodeld/MeshtasticDataModelV 53.xcdatamodel/contents index 8f1c7f57..c1ee4271 100644 --- a/Meshtastic/Meshtastic.xcdatamodeld/MeshtasticDataModelV 53.xcdatamodel/contents +++ b/Meshtastic/Meshtastic.xcdatamodeld/MeshtasticDataModelV 53.xcdatamodel/contents @@ -97,6 +97,7 @@ + diff --git a/Meshtastic/Persistence/UpdateCoreData.swift b/Meshtastic/Persistence/UpdateCoreData.swift index 7aba8899..c241831a 100644 --- a/Meshtastic/Persistence/UpdateCoreData.swift +++ b/Meshtastic/Persistence/UpdateCoreData.swift @@ -581,6 +581,7 @@ func upsertDisplayConfigPacket(config: Config.DisplayConfig, nodeNum: Int64, ses newDisplayConfig.displayMode = Int32(config.displaymode.rawValue) newDisplayConfig.units = Int32(config.units.rawValue) newDisplayConfig.headingBold = config.headingBold + newDisplayConfig.use12HClock = config.use12HClock fetchedNode[0].displayConfig = newDisplayConfig } else { @@ -592,6 +593,7 @@ func upsertDisplayConfigPacket(config: Config.DisplayConfig, nodeNum: Int64, ses fetchedNode[0].displayConfig?.oledType = Int32(config.oled.rawValue) fetchedNode[0].displayConfig?.displayMode = Int32(config.displaymode.rawValue) fetchedNode[0].displayConfig?.units = Int32(config.units.rawValue) + fetchedNode[0].displayConfig?.use12HClock = config.use12HClock fetchedNode[0].displayConfig?.headingBold = config.headingBold } if sessionPasskey != nil { diff --git a/Meshtastic/Views/Settings/Config/DisplayConfig.swift b/Meshtastic/Views/Settings/Config/DisplayConfig.swift index 20a7c521..682bfd45 100644 --- a/Meshtastic/Views/Settings/Config/DisplayConfig.swift +++ b/Meshtastic/Views/Settings/Config/DisplayConfig.swift @@ -27,6 +27,7 @@ struct DisplayConfig: View { @State var oledType = 0 @State var displayMode = 0 @State var units = 0 + @State var use12HourClock = false var body: some View { Form { @@ -74,6 +75,11 @@ struct DisplayConfig: View { .font(.callout) } .pickerStyle(DefaultPickerStyle()) + Toggle(isOn: $use12HourClock) { + Label("12 Hour Clock", systemImage: "clock") + Text("Sets the screen clock format to 12-hour.") + } + .tint(Color.accentColor) } Section(header: Text("Timing & Format")) { VStack(alignment: .leading) { @@ -141,6 +147,7 @@ struct DisplayConfig: View { dc.oled = OledTypes(rawValue: oledType)!.protoEnumValue() dc.displaymode = DisplayModes(rawValue: displayMode)!.protoEnumValue() dc.units = Units(rawValue: units)!.protoEnumValue() + dc.use12HClock = use12HourClock let adminMessageId = bleManager.saveDisplayConfig(config: dc, fromUser: connectedNode!.user!, toUser: node!.user!) if adminMessageId > 0 { @@ -211,6 +218,9 @@ struct DisplayConfig: View { .onChange(of: units) { oldUnits, newUnits in if oldUnits != newUnits && newUnits != node?.displayConfig?.units ?? -1 { hasChanges = true } } + .onChange(of: use12HourClock) { oldUse12HourClock, newUse12HourClock in + if oldUse12HourClock != newUse12HourClock && newUse12HourClock != node?.displayConfig?.use12HClock { hasChanges = true } + } } func setDisplayValues() { self.gpsFormat = Int(node?.displayConfig?.gpsFormat ?? 0) @@ -222,6 +232,7 @@ struct DisplayConfig: View { self.oledType = Int(node?.displayConfig?.oledType ?? 0) self.displayMode = Int(node?.displayConfig?.displayMode ?? 0) self.units = Int(node?.displayConfig?.units ?? 0) - self.hasChanges = false + self.use12HourClock = node?.displayConfig?.use12HClock ?? false + self.hasChanges = node?.displayConfig?.use12HClock ?? false } } From 27ed8200450ba53d80eb18a89e66ae7815596d45 Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Sun, 15 Jun 2025 20:23:42 -0700 Subject: [PATCH 127/213] Add locks to channels in messaging, update share channels icons to match --- Meshtastic/Views/Messages/ChannelList.swift | 7 +++++++ Meshtastic/Views/Settings/ShareChannels.swift | 2 +- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/Meshtastic/Views/Messages/ChannelList.swift b/Meshtastic/Views/Messages/ChannelList.swift index 61f4fe00..fec3ab8b 100644 --- a/Meshtastic/Views/Messages/ChannelList.swift +++ b/Meshtastic/Views/Messages/ChannelList.swift @@ -58,6 +58,13 @@ struct ChannelList: View { VStack(alignment: .leading) { HStack { + if channel.psk?.hexDescription.count ?? 0 < 3 { + Image(systemName: "lock.slash.fill") + .foregroundColor(.red) + } else { + Image(systemName: "lock.fill") + .foregroundColor(.green) + } if channel.name?.isEmpty ?? false { if channel.role == 1 { Text(String("PrimaryChannel").camelCaseToWords()) diff --git a/Meshtastic/Views/Settings/ShareChannels.swift b/Meshtastic/Views/Settings/ShareChannels.swift index fa0e6370..9c7bdaa2 100644 --- a/Meshtastic/Views/Settings/ShareChannels.swift +++ b/Meshtastic/Views/Settings/ShareChannels.swift @@ -83,7 +83,7 @@ struct ShareChannels: View { .labelsHidden() Text(((channel.name!.isEmpty ? "Primary" : channel.name) ?? "Primary").camelCaseToWords()) if channel.psk?.hexDescription.count ?? 0 < 3 { - Image(systemName: "lock.slash") + Image(systemName: "lock.slash.fill") .foregroundColor(.red) } else { Image(systemName: "lock.fill") From 5a753683944a9f973e89a80be0d581ecdda541aa Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Sun, 15 Jun 2025 20:29:47 -0700 Subject: [PATCH 128/213] lock.slash.fill --- Meshtastic/Views/Settings/ShareChannels.swift | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/Meshtastic/Views/Settings/ShareChannels.swift b/Meshtastic/Views/Settings/ShareChannels.swift index 9c7bdaa2..d1c09deb 100644 --- a/Meshtastic/Views/Settings/ShareChannels.swift +++ b/Meshtastic/Views/Settings/ShareChannels.swift @@ -96,7 +96,7 @@ struct ShareChannels: View { .disabled(channel.role == 1) Text(((channel.name!.isEmpty ? "Channel\(channel.index)" : channel.name) ?? "Channel\(channel.index)").camelCaseToWords()).fixedSize() if channel.psk?.hexDescription.count ?? 0 < 3 { - Image(systemName: "lock.slash") + Image(systemName: "lock.slash.fill") .foregroundColor(.red) } else { Image(systemName: "lock.fill") @@ -109,7 +109,7 @@ struct ShareChannels: View { .disabled(channel.role == 1) Text(((channel.name!.isEmpty ? "Channel\(channel.index)" : channel.name) ?? "Channel\(channel.index)").camelCaseToWords()).fixedSize() if channel.psk?.hexDescription.count ?? 0 < 3 { - Image(systemName: "lock.slash") + Image(systemName: "lock.slash.fill") .foregroundColor(.red) } else { Image(systemName: "lock.fill") @@ -122,7 +122,7 @@ struct ShareChannels: View { .disabled(channel.role == 1) Text(((channel.name!.isEmpty ? "Channel\(channel.index)" : channel.name) ?? "Channel\(channel.index)").camelCaseToWords()).fixedSize() if channel.psk?.hexDescription.count ?? 0 < 3 { - Image(systemName: "lock.slash") + Image(systemName: "lock.slash.fill") .foregroundColor(.red) } else { Image(systemName: "lock.fill") @@ -135,7 +135,7 @@ struct ShareChannels: View { .disabled(channel.role == 1) Text(((channel.name!.isEmpty ? "Channel\(channel.index)" : channel.name) ?? "Channel\(channel.index)").camelCaseToWords()).fixedSize() if channel.psk?.hexDescription.count ?? 0 < 3 { - Image(systemName: "lock.slash") + Image(systemName: "lock.slash.fill") .foregroundColor(.red) } else { Image(systemName: "lock.fill") @@ -148,7 +148,7 @@ struct ShareChannels: View { .disabled(channel.role == 1) Text(((channel.name!.isEmpty ? "Channel\(channel.index)" : channel.name) ?? "Channel\(channel.index)").camelCaseToWords()).fixedSize() if channel.psk?.hexDescription.count ?? 0 < 3 { - Image(systemName: "lock.slash") + Image(systemName: "lock.slash.fill") .foregroundColor(.red) } else { Image(systemName: "lock.fill") @@ -161,7 +161,7 @@ struct ShareChannels: View { .disabled(channel.role == 1) Text(((channel.name!.isEmpty ? "Channel\(channel.index)" : channel.name) ?? "Channel\(channel.index)").camelCaseToWords()).fixedSize() if channel.psk?.hexDescription.count ?? 0 < 3 { - Image(systemName: "lock.slash") + Image(systemName: "lock.slash.fill") .foregroundColor(.red) } else { Image(systemName: "lock.fill") @@ -174,7 +174,7 @@ struct ShareChannels: View { .disabled(channel.role == 1) Text(((channel.name!.isEmpty ? "Channel\(channel.index)" : channel.name) ?? "Channel\(channel.index)").camelCaseToWords()).fixedSize() if channel.psk?.hexDescription.count ?? 0 < 3 { - Image(systemName: "lock.slash") + Image(systemName: "lock.slash.fill") .foregroundColor(.red) } else { Image(systemName: "lock.fill") From ee98b5ef77c6dba7cbac57b9e8d5e16e9bf24423 Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Sun, 15 Jun 2025 21:34:36 -0700 Subject: [PATCH 129/213] Bump timer back down --- Meshtastic/Helpers/BLEManager.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Meshtastic/Helpers/BLEManager.swift b/Meshtastic/Helpers/BLEManager.swift index 0dd8ab06..b19ea46d 100644 --- a/Meshtastic/Helpers/BLEManager.swift +++ b/Meshtastic/Helpers/BLEManager.swift @@ -60,7 +60,7 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate private var wantConfigTimer: Timer? private var wantConfigRetryCount = 0 private let maxWantConfigRetries = 3 - private let wantConfigTimeoutInterval: TimeInterval = 10.0 + private let wantConfigTimeoutInterval: TimeInterval = 6.0 // MARK: init private override init() { From 1fae15ea597ff0221455006f4d04324291ab9a15 Mon Sep 17 00:00:00 2001 From: Blackmane <6045018+Blackmane@users.noreply.github.com> Date: Thu, 12 Jun 2025 10:08:05 +0200 Subject: [PATCH 130/213] Update italian translation --- Localizable.xcstrings | 169 +++++++++++++++++++++++++++--------------- 1 file changed, 111 insertions(+), 58 deletions(-) diff --git a/Localizable.xcstrings b/Localizable.xcstrings index d019ab3e..ca923323 100644 --- a/Localizable.xcstrings +++ b/Localizable.xcstrings @@ -1362,7 +1362,7 @@ "it" : { "stringUnit" : { "state" : "translated", - "value" : "🦕 Versione di fine vita 🦖 ☄️" + "value" : "🦕 Versione a fine vita 🦖 ☄️" } }, "sr" : { @@ -1769,7 +1769,7 @@ "it" : { "stringUnit" : { "state" : "translated", - "value" : "Circa" + "value" : "Informazioni" } }, "sr" : { @@ -1903,7 +1903,7 @@ "it" : { "stringUnit" : { "state" : "translated", - "value" : "Riconosciuto" + "value" : "Confermato" } }, "pl" : { @@ -1943,7 +1943,7 @@ "it" : { "stringUnit" : { "state" : "translated", - "value" : "Riconosciuto da un altro nodo" + "value" : "Confermato da un altro nodo" } }, "sr" : { @@ -2152,6 +2152,12 @@ }, "Add Meshtastic Node %@ as a contact" : { "localizations" : { + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Aggiungi nodo Meshtastic %@ ai contatti" + } + }, "zh-Hant-TW" : { "stringUnit" : { "state" : "translated", @@ -2571,7 +2577,7 @@ "it" : { "stringUnit" : { "state" : "translated", - "value" : "Ora d'aria" + "value" : "Tempo di trasmissione" } }, "pl" : { @@ -2611,7 +2617,7 @@ "it" : { "stringUnit" : { "state" : "translated", - "value" : "Allarme" + "value" : "Avviso" } }, "sr" : { @@ -2633,7 +2639,7 @@ "it" : { "stringUnit" : { "state" : "translated", - "value" : "Avvisare il buzzer GPIO quando si riceve un campanello" + "value" : "Attiva il cicalino GPIO alla ricezione di una campana" } }, "sr" : { @@ -2661,7 +2667,7 @@ "it" : { "stringUnit" : { "state" : "translated", - "value" : "Avvisare il cicalino GPIO quando si riceve un messaggio" + "value" : "Attiva il cicalino GPIO alla ricezione di un messaggio" } }, "sr" : { @@ -2683,7 +2689,7 @@ "it" : { "stringUnit" : { "state" : "translated", - "value" : "Avviso GPIO del motore a vibrazione quando si riceve una campana" + "value" : "Attiva la vibrazione GPIO alla ricezione di una campana" } }, "sr" : { @@ -2711,7 +2717,7 @@ "it" : { "stringUnit" : { "state" : "translated", - "value" : "Avviso GPIO del motore vibrante alla ricezione di un messaggio" + "value" : "Attiva la vibrazione GPIO alla ricezione di un messaggio" } }, "sr" : { @@ -2733,7 +2739,7 @@ "it" : { "stringUnit" : { "state" : "translated", - "value" : "Avviso di ricezione di un campanello" + "value" : "Avvisa alla ricezione di una campana" } }, "sr" : { @@ -2761,7 +2767,7 @@ "it" : { "stringUnit" : { "state" : "translated", - "value" : "Avviso di ricezione di un messaggio" + "value" : "Avvisa alla ricezione di un messaggio" } }, "sr" : { @@ -2817,7 +2823,7 @@ "it" : { "stringUnit" : { "state" : "translated", - "value" : "Consentire le richieste di posizione" + "value" : "Consenti le richieste di posizione" } }, "sr" : { @@ -3811,7 +3817,7 @@ "it" : { "stringUnit" : { "state" : "translated", - "value" : "Richiesta negativa" + "value" : "Richiesta non valida" } }, "pl" : { @@ -4649,7 +4655,7 @@ "it" : { "stringUnit" : { "state" : "translated", - "value" : "Trasmette i pacchetti di posizione GPS come priorità." + "value" : "Dà priorità alla trasmissione di pacchetti di posizione GPS." } }, "pl" : { @@ -4707,7 +4713,7 @@ "it" : { "stringUnit" : { "state" : "translated", - "value" : "Trasmette regolarmente la posizione come messaggio al canale predefinito per assistere il recupero del dispositivo." + "value" : "Trasmette regolarmente la posizione come messaggio al canale predefinito per aiutare il recupero del dispositivo." } }, "pl" : { @@ -4833,7 +4839,7 @@ "it" : { "stringUnit" : { "state" : "translated", - "value" : "Acquistare radio complete" + "value" : "Acquista dispositivi completi" } }, "sr" : { @@ -4861,7 +4867,7 @@ "it" : { "stringUnit" : { "state" : "translated", - "value" : "Buzzer GPIO" + "value" : "Cicalino GPIO" } }, "sr" : { @@ -6224,7 +6230,7 @@ "it" : { "stringUnit" : { "state" : "translated", - "value" : "Libero" + "value" : "Svuota" } }, "sr" : { @@ -6960,7 +6966,7 @@ "it" : { "stringUnit" : { "state" : "translated", - "value" : "Radio connessa" + "value" : "Dispositivo connesso" } }, "zh-Hant-TW" : { @@ -7034,7 +7040,7 @@ "it" : { "stringUnit" : { "state" : "translated", - "value" : "La connessione a una nuova radio cancellerà tutti i dati delle app sul telefono." + "value" : "La connessione a un nuovo dispositivo cancellerà tutti i dati dell'applicazione sul telefono." } }, "zh-Hant-TW" : { @@ -7248,7 +7254,7 @@ "it" : { "stringUnit" : { "state" : "translated", - "value" : "Controlla il LED lampeggiante del dispositivo. Per la maggior parte dei dispositivi controlla uno dei 4 LED, mentre i LED del caricatore e del GPS non sono controllabili." + "value" : "Controlla il LED lampeggiante del dispositivo. Per la maggior parte dei dispositivi controlla uno dei 4 LED, mentre quelli di alimentazione e del GPS non sono controllabili." } }, "sr" : { @@ -7282,7 +7288,7 @@ "it" : { "stringUnit" : { "state" : "translated", - "value" : "Scafo convesso" + "value" : "Inviluppo convesso" } }, "sr" : { @@ -7310,7 +7316,7 @@ "it" : { "stringUnit" : { "state" : "translated", - "value" : "Coordinare" + "value" : "Coordinate" } }, "sr" : { @@ -7660,7 +7666,7 @@ "it" : { "stringUnit" : { "state" : "translated", - "value" : "Corrente: %lld" + "value" : "Attuale: %lld" } }, "sr" : { @@ -7682,7 +7688,7 @@ "it" : { "stringUnit" : { "state" : "translated", - "value" : "Attualmente mostra i moduli che potrebbero non essere supportati da questo nodo." + "value" : "Mostra i moduli che potrebbero non essere supportati al momento da questo nodo." } }, "zh-Hant-TW" : { @@ -8088,10 +8094,24 @@ } }, "Delete all config, keys and BLE bonds? " : { - + "localizations" : { + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Cancellare tutte le configurazioni, le chiavi e le associazioni bluetooth?" + } + } + } }, "Delete all config? " : { - + "localizations" : { + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Cancellare tutte le configurazioni?" + } + } + } }, "Delete all device metrics?" : { "localizations" : { @@ -10664,6 +10684,12 @@ }, "Enables the store and forward module." : { "localizations" : { + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Abilita il modulo Salva & Inoltra." + } + }, "zh-Hant-TW" : { "stringUnit" : { "state" : "translated", @@ -10673,10 +10699,24 @@ } }, "Enabling Ethernet will disable the bluetooth connection to the app. TCP node connections are not available on Apple devices." : { - + "localizations" : { + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Abilitando l'Ethernet verrà disabilita la connessione bluetooth all'applicazione. La connessione a nodi TCP non è disponibile su dispositivi Apple." + } + } + } }, "Enabling WiFi will disable the bluetooth connection to the app. TCP node connections are not available on Apple devices." : { - + "localizations" : { + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "L'attivazione del WiFi disabilita la connessione bluetooth all'applicazione. La connessione a nodi TCP non è disponibile su dispositivi Apple." + } + } + } }, "Encoder Press Event" : { "localizations" : { @@ -11580,7 +11620,14 @@ } }, "Factory reset will delete device and app data." : { - + "localizations" : { + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Il ripristino alle impostazioni di fabbrica eliminerà i dati del dispositivo e della app." + } + } + } }, "Failed to encode message content" : { "localizations" : { @@ -11659,7 +11706,7 @@ "it" : { "stringUnit" : { "state" : "translated", - "value" : "Fiera" + "value" : "Discreto" } }, "sr" : { @@ -12471,7 +12518,7 @@ "it" : { "stringUnit" : { "state" : "translated", - "value" : "Seguire" + "value" : "Segui" } }, "pl" : { @@ -12529,7 +12576,7 @@ "it" : { "stringUnit" : { "state" : "translated", - "value" : "Seguire con l'intestazione" + "value" : "Seguire la direzione" } }, "pl" : { @@ -12957,7 +13004,7 @@ "it" : { "stringUnit" : { "state" : "translated", - "value" : "Nome amichevole" + "value" : "Nome semplificato" } }, "sr" : { @@ -13642,7 +13689,7 @@ "it" : { "stringUnit" : { "state" : "translated", - "value" : "Intestazione" + "value" : "Direzione" } }, "sr" : { @@ -13664,7 +13711,7 @@ "it" : { "stringUnit" : { "state" : "translated", - "value" : "Intestazione: %@" + "value" : "Direzione: %@" } }, "sr" : { @@ -13967,7 +14014,7 @@ "it" : { "stringUnit" : { "state" : "translated", - "value" : "Via il luppolo" + "value" : "Distanza in Hop" } }, "sr" : { @@ -17907,7 +17954,7 @@ "it" : { "stringUnit" : { "state" : "translated", - "value" : "La maggior parte dei dati sulla rete viene inviata attraverso il canale primario. È possibile impostare canali secondari per creare gruppi di messaggistica aggiuntivi protetti da una propria chiave. [Suggerimenti per la configurazione del canale](https://meshtastic.org/docs/configuration/tips/)" + "value" : "La maggior parte dei dati sulla rete viene inviata attraverso il canale principale. È possibile impostare canali secondari per creare gruppi di messaggistica aggiuntivi protetti da una propria chiave. [Suggerimenti per la configurazione del canale](https://meshtastic.org/docs/configuration/tips/)" } }, "pl" : { @@ -21101,7 +21148,7 @@ "it" : { "stringUnit" : { "state" : "translated", - "value" : "Modalità di accoppiamento" + "value" : "Modalità di associazione" } }, "pl" : { @@ -22069,7 +22116,7 @@ "it" : { "stringUnit" : { "state" : "translated", - "value" : "Diario di posizione" + "value" : "Registro di posizione" } }, "sr" : { @@ -22711,7 +22758,7 @@ "it" : { "stringUnit" : { "state" : "translated", - "value" : "Primario" + "value" : "Principale" } }, "pl" : { @@ -22785,7 +22832,7 @@ "it" : { "stringUnit" : { "state" : "translated", - "value" : "GPIO primario" + "value" : "GPIO principale" } }, "sr" : { @@ -23327,7 +23374,7 @@ "it" : { "stringUnit" : { "state" : "translated", - "value" : "Test della gamma" + "value" : "Test di portata" } }, "pl" : { @@ -24072,7 +24119,7 @@ "it" : { "stringUnit" : { "state" : "translated", - "value" : "Rimuovere" + "value" : "Elimina" } }, "sr" : { @@ -24122,7 +24169,7 @@ "it" : { "stringUnit" : { "state" : "translated", - "value" : "Rimuovere da ignorato" + "value" : "Elimina da ignorati" } }, "sr" : { @@ -24184,7 +24231,7 @@ "it" : { "stringUnit" : { "state" : "translated", - "value" : "Sostituire i canali" + "value" : "Sostituisci canali" } }, "sr" : { @@ -25508,7 +25555,7 @@ "it" : { "stringUnit" : { "state" : "translated", - "value" : "Sats" + "value" : "Sat" } }, "sr" : { @@ -25536,7 +25583,7 @@ "it" : { "stringUnit" : { "state" : "translated", - "value" : "Stima Sats %lld" + "value" : "Stima satelliti %lld" } }, "sr" : { @@ -25564,7 +25611,7 @@ "it" : { "stringUnit" : { "state" : "translated", - "value" : "Saturazione in vista: %@" + "value" : "Satelliti in vista: %@" } }, "sr" : { @@ -25604,7 +25651,7 @@ "it" : { "stringUnit" : { "state" : "translated", - "value" : "Risparmiare" + "value" : "Salva" } }, "pl" : { @@ -26492,7 +26539,7 @@ "it" : { "stringUnit" : { "state" : "translated", - "value" : "Invia una posizione sul canale primario quando si fa triplo clic sul pulsante utente." + "value" : "Invia una posizione sul canale principale quando si fa triplo clic sul pulsante utente." } }, "sr" : { @@ -26576,7 +26623,7 @@ "it" : { "stringUnit" : { "state" : "translated", - "value" : "Invia una campana ASCII con un messaggio di avviso. Utile per attivare una notifica esterna sul campanello." + "value" : "Invia una campana ASCII con un messaggio di avviso. Utile per attivare notifiche esterne alla ricezione della campana." } }, "sr" : { @@ -28957,6 +29004,12 @@ }, "Standard" : { "localizations" : { + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Predefinito" + } + }, "he" : { "stringUnit" : { "state" : "translated", @@ -29006,7 +29059,7 @@ "it" : { "stringUnit" : { "state" : "translated", - "value" : "Standard Silenzioso" + "value" : "Predefinito Silenzioso" } }, "pl" : { @@ -29126,7 +29179,7 @@ "it" : { "stringUnit" : { "state" : "translated", - "value" : "Memorizzare e inoltrare" + "value" : "Salva & Inoltra" } }, "sr" : { @@ -29154,7 +29207,7 @@ "it" : { "stringUnit" : { "state" : "translated", - "value" : "Configurazione Store & Forward" + "value" : "Configurazione Salva & Inoltra" } }, "sr" : { @@ -33190,7 +33243,7 @@ "it" : { "stringUnit" : { "state" : "translated", - "value" : "Utilizzare un'uscita PWM (come il cicalino RAK) per le sintonie invece di un'uscita on/off. In questo modo si ignorano le impostazioni di uscita, durata e attivazione e si utilizza invece l'opzione GPIO del buzzer configurata dal dispositivo." + "value" : "Utilizzare un'uscita PWM (come il cicalino RAK) per le sintonie invece di un'uscita on/off. In questo modo si ignorano le impostazioni di uscita, durata e attivazione e si utilizza invece l'opzione GPIO del cicalino configurata dal dispositivo." } }, "sr" : { @@ -35016,7 +35069,7 @@ "it" : { "stringUnit" : { "state" : "translated", - "value" : "La frequenza operativa del nodo viene calcolata in base alla regione, alla preimpostazione del modem e a questo campo. Se il campo è 0, lo slot viene calcolato automaticamente in base al nome del canale primario." + "value" : "La frequenza operativa del nodo viene calcolata in base alla regione, alla preimpostazione del modem e a questo campo. Se il campo è 0, lo slot viene calcolato automaticamente in base al nome del canale principale." } }, "sr" : { From b026650435e393081ec92adf7ae688e54883c219 Mon Sep 17 00:00:00 2001 From: Jake-B Date: Mon, 16 Jun 2025 17:24:54 -0400 Subject: [PATCH 131/213] Security settings improvements --- Meshtastic/Views/Helpers/SecureInput.swift | 19 ++++++--- .../Settings/Config/SecurityConfig.swift | 40 ++++++++++++++++++- 2 files changed, 52 insertions(+), 7 deletions(-) diff --git a/Meshtastic/Views/Helpers/SecureInput.swift b/Meshtastic/Views/Helpers/SecureInput.swift index aaed8bd1..687cc6fe 100644 --- a/Meshtastic/Views/Helpers/SecureInput.swift +++ b/Meshtastic/Views/Helpers/SecureInput.swift @@ -12,19 +12,28 @@ struct SecureInput: View { private var idiom: UIUserInterfaceIdiom { UIDevice.current.userInterfaceIdiom } @Binding private var text: String @Binding private var isValid: Bool - @State var isSecure: Bool = true private var title: String - init(_ title: String, text: Binding, isValid: Binding) { + // Local state to store the value of iSSecure, or optionally a binding + private var isSecureBinding: Binding? + @State private var isSecureLocal: Bool = true + + private var isSecure: Binding { + // Use the binding if we have one, otherwise fallback to the local state variable + isSecureBinding ?? $isSecureLocal + } + + init(_ title: String, text: Binding, isValid: Binding, isSecure: Binding? = nil) { self.title = title self._text = text self._isValid = isValid + self.isSecureBinding = isSecure } var body: some View { ZStack(alignment: .trailing) { Group { - if isSecure { + if isSecure.wrappedValue { SecureField(title, text: $text) .font(idiom == .phone ? .caption : .callout) .allowsTightening(true) @@ -51,9 +60,9 @@ struct SecureInput: View { if !text.isEmpty { Button(action: { - isSecure.toggle() + isSecure.wrappedValue.toggle() }) { - Image(systemName: self.isSecure ? "eye.slash" : "eye") + Image(systemName: self.isSecure.wrappedValue ? "eye.slash" : "eye") .accentColor(.secondary) }.buttonStyle(BorderlessButtonStyle()) } diff --git a/Meshtastic/Views/Settings/Config/SecurityConfig.swift b/Meshtastic/Views/Settings/Config/SecurityConfig.swift index 2095ae9b..7c22bcf3 100644 --- a/Meshtastic/Views/Settings/Config/SecurityConfig.swift +++ b/Meshtastic/Views/Settings/Config/SecurityConfig.swift @@ -10,6 +10,7 @@ import SwiftUI import CoreData import MeshtasticProtobufs import OSLog +import CryptoKit struct SecurityConfig: View { @@ -33,6 +34,15 @@ struct SecurityConfig: View { @State var isManaged = false @State var serialEnabled = false @State var debugLogApiEnabled = false + @State var privateKeyIsSecure = true + + private var isValidKeyPair: Bool { + if let privateKeyBytes = Data(base64Encoded: privateKey), + let calculatedPublicKey = generatePublicKey(from: privateKeyBytes) { + return calculatedPublicKey.base64EncodedString() == publicKey + } + return false + } var body: some View { VStack { @@ -51,12 +61,16 @@ struct SecurityConfig: View { .foregroundStyle(.tertiary) .disableAutocorrection(true) .textSelection(.enabled) + .background( + RoundedRectangle(cornerRadius: 10.0) + .stroke(isValidKeyPair ? Color.clear : Color.red, lineWidth: 2.0) + ) Text("Sent out to other nodes on the mesh to allow them to compute a shared secret key.") .foregroundStyle(.secondary) .font(idiom == .phone ? .caption : .callout) Divider() Label("Private Key", systemImage: "key.fill") - SecureInput("Private Key", text: $privateKey, isValid: $hasValidPrivateKey) + SecureInput("Private Key", text: $privateKey, isValid: $hasValidPrivateKey, isSecure: $privateKeyIsSecure) .background( RoundedRectangle(cornerRadius: 10.0) .stroke(hasValidPrivateKey ? Color.clear : Color.red, lineWidth: 2.0) @@ -70,6 +84,7 @@ struct SecurityConfig: View { Button { if let keyBytes = generatePrivateKey(count: 32) { privateKey = keyBytes.base64EncodedString() + self.privateKeyIsSecure = false } } label: { Image(systemName: "lock.rotation") @@ -156,6 +171,10 @@ struct SecurityConfig: View { let tempKey = Data(base64Encoded: privateKey) ?? Data() if tempKey.count == 32 { hasValidPrivateKey = true + if let privateKeyBytes = Data(base64Encoded: privateKey), privateKeyBytes.count == 32 { + // Valid private key -- generate the public key + publicKey = generatePublicKey(from: privateKeyBytes)?.base64EncodedString() ?? "" + } } else { hasValidPrivateKey = false } @@ -287,7 +306,24 @@ struct SecurityConfig: View { return randomBytes } else { // Handle error, perhaps by logging or throwing an exception - print("Error generating random bytes: \(status)") + Logger.mesh.debug("Error generating random bytes: \(status)") + return nil + } + } + + func generatePublicKey(from privateKeyData: Data) -> Data? { + guard privateKeyData.count == 32 else { + Logger.mesh.debug("Invalid private key length. Must be 32 bytes for Curve25519.") + return nil + } + + do { + // Create a Curve25519 private key from raw representation + let privateKey = try Curve25519.KeyAgreement.PrivateKey(rawRepresentation: privateKeyData) + let publicKey = privateKey.publicKey + return publicKey.rawRepresentation + } catch { + Logger.mesh.debug("Failed to create Curve25519 key: \(error)") return nil } } From 3c5c78f10fb1485a58d62594bb13a27347bac4de Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Mon, 16 Jun 2025 15:12:42 -0700 Subject: [PATCH 132/213] 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 58b50820..6f72607e 100644 --- a/Meshtastic.xcodeproj/project.pbxproj +++ b/Meshtastic.xcodeproj/project.pbxproj @@ -1808,7 +1808,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 2.6.6; + MARKETING_VERSION = 2.6.7; PRODUCT_BUNDLE_IDENTIFIER = gvh.MeshtasticClient; PRODUCT_NAME = "$(TARGET_NAME)"; SUPPORTS_MACCATALYST = YES; @@ -1841,7 +1841,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 2.6.6; + MARKETING_VERSION = 2.6.7; PRODUCT_BUNDLE_IDENTIFIER = gvh.MeshtasticClient; PRODUCT_NAME = "$(TARGET_NAME)"; SUPPORTS_MACCATALYST = YES; @@ -1872,7 +1872,7 @@ "@executable_path/Frameworks", "@executable_path/../../Frameworks", ); - MARKETING_VERSION = 2.6.6; + MARKETING_VERSION = 2.6.7; PRODUCT_BUNDLE_IDENTIFIER = gvh.MeshtasticClient.Widgets; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; @@ -1904,7 +1904,7 @@ "@executable_path/Frameworks", "@executable_path/../../Frameworks", ); - MARKETING_VERSION = 2.6.6; + MARKETING_VERSION = 2.6.7; PRODUCT_BUNDLE_IDENTIFIER = gvh.MeshtasticClient.Widgets; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; From d93064cd2966b4452445d42aa301498a0cab5c63 Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Mon, 16 Jun 2025 15:51:27 -0700 Subject: [PATCH 133/213] Update Meshtastic/Views/Settings/Config/SecurityConfig.swift Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- Meshtastic/Views/Settings/Config/SecurityConfig.swift | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/Meshtastic/Views/Settings/Config/SecurityConfig.swift b/Meshtastic/Views/Settings/Config/SecurityConfig.swift index 7c22bcf3..3b236fb2 100644 --- a/Meshtastic/Views/Settings/Config/SecurityConfig.swift +++ b/Meshtastic/Views/Settings/Config/SecurityConfig.swift @@ -37,11 +37,12 @@ struct SecurityConfig: View { @State var privateKeyIsSecure = true private var isValidKeyPair: Bool { - if let privateKeyBytes = Data(base64Encoded: privateKey), - let calculatedPublicKey = generatePublicKey(from: privateKeyBytes) { - return calculatedPublicKey.base64EncodedString() == publicKey + guard let privateKeyBytes = Data(base64Encoded: privateKey), + let calculatedPublicKey = generatePublicKey(from: privateKeyBytes), + let decodedPublicKey = Data(base64Encoded: publicKey) else { + return false } - return false + return calculatedPublicKey == decodedPublicKey } var body: some View { From f191877376f2514815b400ee2f59509f8680a6a4 Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Tue, 17 Jun 2025 09:11:51 -0700 Subject: [PATCH 134/213] Show updated public key if the private key is changed, regeneration still takes place in the firmware. Remove now unnessary reboot after saving a new private key --- .../Settings/Config/SecurityConfig.swift | 28 ++++++++++--------- 1 file changed, 15 insertions(+), 13 deletions(-) diff --git a/Meshtastic/Views/Settings/Config/SecurityConfig.swift b/Meshtastic/Views/Settings/Config/SecurityConfig.swift index 3b236fb2..5954f47d 100644 --- a/Meshtastic/Views/Settings/Config/SecurityConfig.swift +++ b/Meshtastic/Views/Settings/Config/SecurityConfig.swift @@ -38,7 +38,7 @@ struct SecurityConfig: View { private var isValidKeyPair: Bool { guard let privateKeyBytes = Data(base64Encoded: privateKey), - let calculatedPublicKey = generatePublicKey(from: privateKeyBytes), + let calculatedPublicKey = generatePublicKeyDisplay(from: privateKeyBytes), let decodedPublicKey = Data(base64Encoded: publicKey) else { return false } @@ -174,7 +174,7 @@ struct SecurityConfig: View { hasValidPrivateKey = true if let privateKeyBytes = Data(base64Encoded: privateKey), privateKeyBytes.count == 32 { // Valid private key -- generate the public key - publicKey = generatePublicKey(from: privateKeyBytes)?.base64EncodedString() ?? "" + publicKey = generatePublicKeyDisplay(from: privateKeyBytes)?.base64EncodedString() ?? "" } } else { hasValidPrivateKey = false @@ -251,15 +251,13 @@ struct SecurityConfig: View { } var config = Config.SecurityConfig() - config.publicKey = Data(base64Encoded: publicKey) ?? Data() config.privateKey = Data(base64Encoded: privateKey) ?? Data() config.adminKey = [Data(base64Encoded: adminKey) ?? Data(), Data(base64Encoded: adminKey2) ?? Data(), Data(base64Encoded: adminKey3) ?? Data()] config.isManaged = isManaged config.serialEnabled = serialEnabled config.debugLogApiEnabled = debugLogApiEnabled - let reboot = node?.securityConfig?.privateKey?.base64EncodedString() ?? "" != privateKey - + let keyUpdated = node?.securityConfig?.privateKey?.base64EncodedString() ?? "" != privateKey let adminMessageId = bleManager.saveSecurityConfig( config: config, fromUser: fromUser, @@ -268,15 +266,18 @@ struct SecurityConfig: View { if adminMessageId > 0 { // Should show a saved successfully alert once I know that to be true // for now just disable the button after a successful save - hasChanges = false - if reboot { - if !bleManager.sendReboot( - fromUser: fromUser, - toUser: toUser - ) { - Logger.mesh.warning("Reboot Failed") + if keyUpdated { + node?.user?.publicKey = Data(base64Encoded: publicKey) ?? Data() + do { + try context.save() + Logger.data.info("💾 Saved UserEntity Public Key to Core Data for \(node?.num ?? 0, privacy: .public)") + } catch { + context.rollback() + let nsError = error as NSError + Logger.data.error("Error Updating Core Data UserEntity: \(nsError, privacy: .public)") } } + hasChanges = false goBack() } } @@ -312,7 +313,8 @@ struct SecurityConfig: View { } } - func generatePublicKey(from privateKeyData: Data) -> Data? { + // Generate a new public key for display purposes to show the user what will be changed after the new private key is saved to the device + func generatePublicKeyDisplay(from privateKeyData: Data) -> Data? { guard privateKeyData.count == 32 else { Logger.mesh.debug("Invalid private key length. Must be 32 bytes for Curve25519.") return nil From db0119bed9b402b44e374d451602f3c318e558da Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Tue, 17 Jun 2025 16:33:11 -0700 Subject: [PATCH 135/213] Prevent users from disconnecting from bluetooth when want config is occuring let the timer do its job, clean up want config timer logic to occur in one place --- Localizable.xcstrings | 12 ++-- Meshtastic/Helpers/BLEManager.swift | 35 ++++------ Meshtastic/Views/Bluetooth/Connect.swift | 86 +++++++++++++----------- 3 files changed, 65 insertions(+), 68 deletions(-) diff --git a/Localizable.xcstrings b/Localizable.xcstrings index ca923323..9a6c28c1 100644 --- a/Localizable.xcstrings +++ b/Localizable.xcstrings @@ -29004,18 +29004,18 @@ }, "Standard" : { "localizations" : { - "it" : { - "stringUnit" : { - "state" : "translated", - "value" : "Predefinito" - } - }, "he" : { "stringUnit" : { "state" : "translated", "value" : "סטנדרטי" } }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Predefinito" + } + }, "pl" : { "stringUnit" : { "state" : "translated", diff --git a/Meshtastic/Helpers/BLEManager.swift b/Meshtastic/Helpers/BLEManager.swift index b19ea46d..81f0ff70 100644 --- a/Meshtastic/Helpers/BLEManager.swift +++ b/Meshtastic/Helpers/BLEManager.swift @@ -59,8 +59,8 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate private var wantConfigTimer: Timer? private var wantConfigRetryCount = 0 - private let maxWantConfigRetries = 3 - private let wantConfigTimeoutInterval: TimeInterval = 6.0 + private let maxWantConfigRetries = 2 + private let wantConfigTimeoutInterval: TimeInterval = 5.0 // MARK: init private override init() { @@ -193,7 +193,12 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate if self.mqttProxyConnected { self.mqttManager.mqttClientProxy?.disconnect() } - self.wantConfigTimer?.invalidate() + self.isWaitingForWantConfigResponse = false + if wantConfigTimer != nil { + self.wantConfigTimer?.invalidate() + } + self.wantConfigTimer = nil + self.wantConfigRetryCount = 0 self.automaticallyReconnect = reconnect self.centralManager?.cancelPeripheralConnection(connectedPeripheral.peripheral) self.FROMRADIO_characteristic = nil @@ -506,22 +511,22 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate } return success } - + func sendWantConfig() { isWaitingForWantConfigResponse = true - + guard connectedPeripheral?.peripheral.state ?? CBPeripheralState.disconnected == CBPeripheralState.connected else { return } - + if FROMRADIO_characteristic == nil { Logger.mesh.error("🚨 \("Unsupported Firmware Version Detected, unable to connect to device.".localized, privacy: .public)") invalidVersion = true return } else { - + let nodeName = connectedPeripheral?.peripheral.name ?? "Unknown".localized let logString = String.localizedStringWithFormat("Issuing Want Config to %@".localized, nodeName) Logger.mesh.info("🛎️ \(logString, privacy: .public)") - + // BLE Characteristics discovered, issue wantConfig var toRadio: ToRadio = ToRadio() configNonce = UInt32(NONCE_ONLY_DB) @@ -533,11 +538,9 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate return } connectedPeripheral!.peripheral.writeValue(binaryData, for: TORADIO_characteristic, type: .withResponse) - // Either Read the config complete value or from num notify value guard connectedPeripheral != nil else { return } connectedPeripheral!.peripheral.readValue(for: FROMRADIO_characteristic) - // Start timeout timer startWantConfigTimeout() } @@ -546,7 +549,6 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate private func startWantConfigTimeout() { // Cancel any existing timer wantConfigTimer?.invalidate() - // Start new timer wantConfigTimer = Timer.scheduledTimer(withTimeInterval: wantConfigTimeoutInterval, repeats: false) { [weak self] _ in self?.handleWantConfigTimeout() @@ -555,9 +557,7 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate private func handleWantConfigTimeout() { guard isWaitingForWantConfigResponse else { return } - wantConfigRetryCount += 1 - if wantConfigRetryCount < maxWantConfigRetries { Logger.mesh.warning("⏰ Want Config timeout, retrying... (attempt \(self.wantConfigRetryCount + 1)/\(self.maxWantConfigRetries))") sendWantConfig() @@ -577,15 +577,8 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate } private func forceDisconnect() { - isWaitingForWantConfigResponse = false - wantConfigTimer?.invalidate() - wantConfigTimer = nil - wantConfigRetryCount = 0 - disconnectPeripheral(reconnect: false) - - lastConnectionError = "Bluetooth connection timeout, keep your node closer.".localized - + lastConnectionError = "Bluetooth connection timeout, keep your node closer or reboot your radio if the problem continues.".localized Logger.mesh.error("💥 [BLE] Forced disconnect due to Want Config timeout") } diff --git a/Meshtastic/Views/Bluetooth/Connect.swift b/Meshtastic/Views/Bluetooth/Connect.swift index 4e114bb9..5e73270e 100644 --- a/Meshtastic/Views/Bluetooth/Connect.swift +++ b/Meshtastic/Views/Bluetooth/Connect.swift @@ -90,40 +90,7 @@ struct Connect: View { .foregroundColor(Color.gray) .padding([.top]) .swipeActions { - Button(role: .destructive) { - if let connectedPeripheral = bleManager.connectedPeripheral, - connectedPeripheral.peripheral.state == .connected { - bleManager.disconnectPeripheral(reconnect: false) - } - } label: { - Label("Disconnect", systemImage: "antenna.radiowaves.left.and.right.slash") - } - } - .contextMenu { - - if node != nil { - #if !targetEnvironment(macCatalyst) - Button { - if !liveActivityStarted { - #if canImport(ActivityKit) - Logger.services.info("Start live activity.") - startNodeActivity() - #endif - } else { - #if canImport(ActivityKit) - Logger.services.info("Stop live activity.") - endActivity() - #endif - } - } label: { - Label("Mesh Live Activity", systemImage: liveActivityStarted ? "stop" : "play") - } - #endif - Text("Num: \(String(node!.num))") - Text("Short Name: \(node?.user?.shortName ?? "?")") - Text("Long Name: \(node?.user?.longName?.addingVariationSelectors ?? "Unknown".localized)") - Text("BLE RSSI: \(connectedPeripheral.rssi)") - + if bleManager.isSubscribed { Button(role: .destructive) { if let connectedPeripheral = bleManager.connectedPeripheral, connectedPeripheral.peripheral.state == .connected { @@ -132,13 +99,51 @@ struct Connect: View { } label: { Label("Disconnect", systemImage: "antenna.radiowaves.left.and.right.slash") } - Button { - if !bleManager.sendShutdown(fromUser: node!.user!, toUser: node!.user!) { - Logger.mesh.error("Shutdown Failed") - } + } + } + .contextMenu { - } label: { - Label("Power Off", systemImage: "power") + if node != nil { + #if !targetEnvironment(macCatalyst) + if bleManager.isSubscribed { + Button { + if !liveActivityStarted { + #if canImport(ActivityKit) + Logger.services.info("Start live activity.") + startNodeActivity() + #endif + } else { + #if canImport(ActivityKit) + Logger.services.info("Stop live activity.") + endActivity() + #endif + } + } label: { + Label("Mesh Live Activity", systemImage: liveActivityStarted ? "stop" : "play") + } + } + #endif + Text("Num: \(String(node!.num))") + Text("Short Name: \(node?.user?.shortName ?? "?")") + Text("Long Name: \(node?.user?.longName?.addingVariationSelectors ?? "Unknown".localized)") + Text("BLE RSSI: \(connectedPeripheral.rssi)") + if bleManager.isSubscribed { + Button(role: .destructive) { + if let connectedPeripheral = bleManager.connectedPeripheral, + connectedPeripheral.peripheral.state == .connected { + bleManager.disconnectPeripheral(reconnect: false) + } + } label: { + Label("Disconnect", systemImage: "antenna.radiowaves.left.and.right.slash") + } + Button { + if !bleManager.sendShutdown(fromUser: node!.user!, toUser: node!.user!) { + Logger.mesh.error("Shutdown Failed") + } + + } label: { + Label("Power Off", systemImage: "power") + } } } } @@ -154,7 +159,6 @@ struct Connect: View { } } } else { - if bleManager.isConnecting { HStack { Image(systemName: "antenna.radiowaves.left.and.right") From aafa7b7b31f926cc151adc027082e61db6a0e1d6 Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Tue, 17 Jun 2025 17:14:45 -0700 Subject: [PATCH 136/213] Use want config timer to prevent disconnect while config is running, stop force disconnecting. --- Meshtastic/Helpers/BLEManager.swift | 12 +++++------- Meshtastic/Views/Bluetooth/Connect.swift | 4 ++-- 2 files changed, 7 insertions(+), 9 deletions(-) diff --git a/Meshtastic/Helpers/BLEManager.swift b/Meshtastic/Helpers/BLEManager.swift index 81f0ff70..84c22341 100644 --- a/Meshtastic/Helpers/BLEManager.swift +++ b/Meshtastic/Helpers/BLEManager.swift @@ -32,6 +32,7 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate public var isConnecting: Bool = false public var isConnected: Bool = false public var isSubscribed: Bool = false + public var allowDisconnect: Bool = false private var configNonce: UInt32 = 1 var timeoutTimer: Timer? var timeoutTimerCount = 0 @@ -172,6 +173,7 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate isConnecting = false isConnected = false isSubscribed = false + allowDisconnect = false self.connectedPeripheral = nil invalidVersion = false connectedVersion = "0.0.0" @@ -204,6 +206,7 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate self.FROMRADIO_characteristic = nil self.isConnected = false self.isSubscribed = false + self.allowDisconnect = false self.invalidVersion = false self.connectedVersion = "0.0.0" self.stopScanning() @@ -563,7 +566,7 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate sendWantConfig() } else { Logger.mesh.error("🚨 Want Config failed after \(self.maxWantConfigRetries) attempts, forcing disconnect") - forceDisconnect() + lastConnectionError = "Bluetooth connection timeout, keep your node closer or reboot your radio if the problem continues.".localized } } @@ -576,12 +579,6 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate } } - private func forceDisconnect() { - disconnectPeripheral(reconnect: false) - lastConnectionError = "Bluetooth connection timeout, keep your node closer or reboot your radio if the problem continues.".localized - Logger.mesh.error("💥 [BLE] Forced disconnect due to Want Config timeout") - } - // Call this to reset the retry mechanism (e.g., on new connection) func resetWantConfigRetries() { wantConfigRetryCount = 0 @@ -1059,6 +1056,7 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate invalidVersion = false lastConnectionError = "" isSubscribed = true + allowDisconnect = true Logger.mesh.info("🤜 [BLE] Want Config Complete. ID:\(decodedInfo.configCompleteID, privacy: .public)") if sendTime() { } diff --git a/Meshtastic/Views/Bluetooth/Connect.swift b/Meshtastic/Views/Bluetooth/Connect.swift index 5e73270e..1176241a 100644 --- a/Meshtastic/Views/Bluetooth/Connect.swift +++ b/Meshtastic/Views/Bluetooth/Connect.swift @@ -90,7 +90,7 @@ struct Connect: View { .foregroundColor(Color.gray) .padding([.top]) .swipeActions { - if bleManager.isSubscribed { + if bleManager.allowDisconnect { Button(role: .destructive) { if let connectedPeripheral = bleManager.connectedPeripheral, connectedPeripheral.peripheral.state == .connected { @@ -127,7 +127,7 @@ struct Connect: View { Text("Short Name: \(node?.user?.shortName ?? "?")") Text("Long Name: \(node?.user?.longName?.addingVariationSelectors ?? "Unknown".localized)") Text("BLE RSSI: \(connectedPeripheral.rssi)") - if bleManager.isSubscribed { + if bleManager.allowDisconnect { Button(role: .destructive) { if let connectedPeripheral = bleManager.connectedPeripheral, connectedPeripheral.peripheral.state == .connected { From d55a528f7fb3f759bc1d157f7936dd54895b163f Mon Sep 17 00:00:00 2001 From: Benjamin Faershtein <119711889+RCGV1@users.noreply.github.com> Date: Tue, 17 Jun 2025 17:25:30 -0700 Subject: [PATCH 137/213] Update factory reset and restart intents --- Localizable.xcstrings | 14 ++++++++++++++ Meshtastic.xcodeproj/project.pbxproj | 4 ---- Meshtastic/AppIntents/FactoryResetNodeIntent.swift | 14 +++++++++++--- Meshtastic/AppIntents/RestartNodeIntent.swift | 1 - 4 files changed, 25 insertions(+), 8 deletions(-) diff --git a/Localizable.xcstrings b/Localizable.xcstrings index 9a6c28c1..08ef7fcb 100644 --- a/Localizable.xcstrings +++ b/Localizable.xcstrings @@ -13633,6 +13633,9 @@ } } } + }, + "Hard Reset" : { + }, "Hardware" : { "localizations" : { @@ -14972,6 +14975,9 @@ } } } + }, + "In addition to Config, Keys and BLE bonds will be wiped" : { + }, "Include" : { "localizations" : { @@ -23026,6 +23032,9 @@ } } } + }, + "Provide Confirmation" : { + }, "Public Key" : { "localizations" : { @@ -26425,6 +26434,7 @@ } }, "Send a Direct Message" : { + "extractionState" : "stale", "localizations" : { "it" : { "stringUnit" : { @@ -26513,6 +26523,7 @@ } }, "Send a message to a certain meshtastic node" : { + "extractionState" : "stale", "localizations" : { "it" : { "stringUnit" : { @@ -28178,6 +28189,9 @@ } } } + }, + "Show a confirmation dialog before performing the factory reset" : { + }, "Show alerts" : { "localizations" : { diff --git a/Meshtastic.xcodeproj/project.pbxproj b/Meshtastic.xcodeproj/project.pbxproj index 6f72607e..7584a821 100644 --- a/Meshtastic.xcodeproj/project.pbxproj +++ b/Meshtastic.xcodeproj/project.pbxproj @@ -59,7 +59,6 @@ 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 */; }; - BC47C2EF2CE0017D008245CA /* MessageNodeIntent.swift in Sources */ = {isa = PBXBuildFile; fileRef = BC47C2EE2CE0017D008245CA /* MessageNodeIntent.swift */; }; BC6B45FF2CB2F98900723CEB /* SaveChannelSettingsIntent.swift in Sources */ = {isa = PBXBuildFile; fileRef = BC6B45FE2CB2F98900723CEB /* SaveChannelSettingsIntent.swift */; }; BCB613812C67290800485544 /* SendWaypointIntent.swift in Sources */ = {isa = PBXBuildFile; fileRef = BCB613802C67290800485544 /* SendWaypointIntent.swift */; }; BCB613832C672A2600485544 /* MessageChannelIntent.swift in Sources */ = {isa = PBXBuildFile; fileRef = BCB613822C672A2600485544 /* MessageChannelIntent.swift */; }; @@ -325,7 +324,6 @@ B399E8A32B6F486400E4488E /* RetryButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RetryButton.swift; sourceTree = ""; }; B3E905B02B71F7F300654D07 /* TextMessageField.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TextMessageField.swift; sourceTree = ""; }; BC10380E2DD4333C00B00BFA /* AddContactIntent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddContactIntent.swift; sourceTree = ""; }; - BC47C2EE2CE0017D008245CA /* MessageNodeIntent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageNodeIntent.swift; sourceTree = ""; }; BC5EBA3B2D002A2000C442FF /* MessageNodeIntent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageNodeIntent.swift; sourceTree = ""; }; BC6B45FE2CB2F98900723CEB /* SaveChannelSettingsIntent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SaveChannelSettingsIntent.swift; sourceTree = ""; }; BCB613802C67290800485544 /* SendWaypointIntent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SendWaypointIntent.swift; sourceTree = ""; }; @@ -694,7 +692,6 @@ BCE2D3C82C7C377F008E6199 /* FactoryResetNodeIntent.swift */, BCE2D3C62C7B0D0A008E6199 /* ShortcutsProvider.swift */, BC6B45FE2CB2F98900723CEB /* SaveChannelSettingsIntent.swift */, - BC47C2EE2CE0017D008245CA /* MessageNodeIntent.swift */, BCD93CB92D9E11A2006C9214 /* DisconnectNodeIntent.swift */, ); path = AppIntents; @@ -1378,7 +1375,6 @@ DDFFA7472B3A7F3C004730DB /* Bundle.swift in Sources */, DD457188293C7E63000C49FB /* BLESignalStrengthIndicator.swift in Sources */, DDA9515C2BC6631200CEA535 /* TelemetryEnums.swift in Sources */, - BC47C2EF2CE0017D008245CA /* MessageNodeIntent.swift in Sources */, DDFEB3BB29900C1200EE7472 /* CurrentConditionsCompact.swift in Sources */, DD836AE726F6B38600ABCC23 /* Connect.swift in Sources */, D93069082B81DF040066FBC8 /* SaveConfigButton.swift in Sources */, diff --git a/Meshtastic/AppIntents/FactoryResetNodeIntent.swift b/Meshtastic/AppIntents/FactoryResetNodeIntent.swift index 60946b00..9994c19d 100644 --- a/Meshtastic/AppIntents/FactoryResetNodeIntent.swift +++ b/Meshtastic/AppIntents/FactoryResetNodeIntent.swift @@ -11,11 +11,19 @@ import AppIntents struct FactoryResetNodeIntent: AppIntent { static var title: LocalizedStringResource = "Factory Reset" static var description: IntentDescription = "Perform a factory reset on the node you are connected to" + + @Parameter(title: "Hard Reset", description: "In addition to Config, Keys and BLE bonds will be wiped", default: false) + var hardReset: Bool + + @Parameter(title: "Provide Confirmation", description: "Show a confirmation dialog before performing the factory reset", default: true) + var provideConfirmation: Bool func perform() async throws -> some IntentResult { // Request user confirmation before performing the factory reset - try await requestConfirmation(result: .result(dialog: "Are you sure you want to factory reset the node?"), confirmationActionName: ConfirmationActionName - .custom(acceptLabel: "Factory Reset", acceptAlternatives: [], denyLabel: "Cancel", denyAlternatives: [], destructive: true)) + if provideConfirmation { + try await requestConfirmation(result: .result(dialog: "Are you sure you want to factory reset the node?"), confirmationActionName: ConfirmationActionName + .custom(acceptLabel: "Factory Reset", acceptAlternatives: [], denyLabel: "Cancel", denyAlternatives: [], destructive: true)) + } // Ensure the node is connected if !BLEManager.shared.isConnected { @@ -29,7 +37,7 @@ struct FactoryResetNodeIntent: AppIntent { let toUser = connectedNode.user { // Attempt to send a factory reset command, throw an error if it fails - if !BLEManager.shared.sendFactoryReset(fromUser: fromUser, toUser: toUser) { + if !BLEManager.shared.sendFactoryReset(fromUser: fromUser, toUser: toUser, resetDevice: hardReset) { throw AppIntentErrors.AppIntentError.message("Failed to perform factory reset") } } else { diff --git a/Meshtastic/AppIntents/RestartNodeIntent.swift b/Meshtastic/AppIntents/RestartNodeIntent.swift index 3859c114..bff6affb 100644 --- a/Meshtastic/AppIntents/RestartNodeIntent.swift +++ b/Meshtastic/AppIntents/RestartNodeIntent.swift @@ -15,7 +15,6 @@ struct RestartNodeIntent: AppIntent { func perform() async throws -> some IntentResult { - try await requestConfirmation(result: .result(dialog: "Reboot node?")) if !BLEManager.shared.isConnected { throw AppIntentErrors.AppIntentError.notConnected From 128b7df3f55bf8f4845bc53e394f3a08108208ca Mon Sep 17 00:00:00 2001 From: Benjamin Faershtein <119711889+RCGV1@users.noreply.github.com> Date: Tue, 17 Jun 2025 19:06:54 -0700 Subject: [PATCH 138/213] Fixed connection issues by first sending heartbeat then wantConfig to not do a packet dupe that gets ignored --- Meshtastic/Helpers/BLEManager.swift | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/Meshtastic/Helpers/BLEManager.swift b/Meshtastic/Helpers/BLEManager.swift index 84c22341..057defa1 100644 --- a/Meshtastic/Helpers/BLEManager.swift +++ b/Meshtastic/Helpers/BLEManager.swift @@ -525,11 +525,20 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate invalidVersion = true return } else { + + // Send Heartbeat before wantConfig + var heartbeatToRadio: ToRadio = ToRadio() + heartbeatToRadio.payloadVariant = .heartbeat(Heartbeat()) + guard let heartbeatBinaryData: Data = try? heartbeatToRadio.serializedData() else { + Logger.mesh.error("Failed to serialize Heartbeat ToRadio message") + return + } + connectedPeripheral!.peripheral.writeValue(heartbeatBinaryData, for: TORADIO_characteristic, type: .withResponse) let nodeName = connectedPeripheral?.peripheral.name ?? "Unknown".localized let logString = String.localizedStringWithFormat("Issuing Want Config to %@".localized, nodeName) Logger.mesh.info("🛎️ \(logString, privacy: .public)") - + // BLE Characteristics discovered, issue wantConfig var toRadio: ToRadio = ToRadio() configNonce = UInt32(NONCE_ONLY_DB) @@ -567,6 +576,7 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate } else { Logger.mesh.error("🚨 Want Config failed after \(self.maxWantConfigRetries) attempts, forcing disconnect") lastConnectionError = "Bluetooth connection timeout, keep your node closer or reboot your radio if the problem continues.".localized + disconnectPeripheral(reconnect: false) } } From 527a1b7966e26c67ba8fa61756d9c64c06e68393 Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Tue, 17 Jun 2025 20:44:52 -0700 Subject: [PATCH 139/213] Remove disconnect that overrides pin screen and allow disconnect if device gets hung up --- Meshtastic/Helpers/BLEManager.swift | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/Meshtastic/Helpers/BLEManager.swift b/Meshtastic/Helpers/BLEManager.swift index 057defa1..d2c4df7f 100644 --- a/Meshtastic/Helpers/BLEManager.swift +++ b/Meshtastic/Helpers/BLEManager.swift @@ -538,7 +538,6 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate let nodeName = connectedPeripheral?.peripheral.name ?? "Unknown".localized let logString = String.localizedStringWithFormat("Issuing Want Config to %@".localized, nodeName) Logger.mesh.info("🛎️ \(logString, privacy: .public)") - // BLE Characteristics discovered, issue wantConfig var toRadio: ToRadio = ToRadio() configNonce = UInt32(NONCE_ONLY_DB) @@ -576,7 +575,7 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate } else { Logger.mesh.error("🚨 Want Config failed after \(self.maxWantConfigRetries) attempts, forcing disconnect") lastConnectionError = "Bluetooth connection timeout, keep your node closer or reboot your radio if the problem continues.".localized - disconnectPeripheral(reconnect: false) + allowDisconnect = true } } From 51d5e85042300deb4fa382797841e83a287db507 Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Tue, 17 Jun 2025 23:05:19 -0700 Subject: [PATCH 140/213] Automatically disconnnect after 6 6 second want config fauilures so the pin dialog has a cheance to time out first --- Meshtastic/Helpers/BLEManager.swift | 10 ++++++---- Meshtastic/Views/Bluetooth/Connect.swift | 2 +- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/Meshtastic/Helpers/BLEManager.swift b/Meshtastic/Helpers/BLEManager.swift index d2c4df7f..930c8fb9 100644 --- a/Meshtastic/Helpers/BLEManager.swift +++ b/Meshtastic/Helpers/BLEManager.swift @@ -60,8 +60,8 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate private var wantConfigTimer: Timer? private var wantConfigRetryCount = 0 - private let maxWantConfigRetries = 2 - private let wantConfigTimeoutInterval: TimeInterval = 5.0 + private let maxWantConfigRetries = 6 + private let wantConfigTimeoutInterval: TimeInterval = 6.0 // MARK: init private override init() { @@ -525,7 +525,6 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate invalidVersion = true return } else { - // Send Heartbeat before wantConfig var heartbeatToRadio: ToRadio = ToRadio() heartbeatToRadio.payloadVariant = .heartbeat(Heartbeat()) @@ -569,13 +568,16 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate private func handleWantConfigTimeout() { guard isWaitingForWantConfigResponse else { return } wantConfigRetryCount += 1 + if wantConfigRetryCount == 1 { + allowDisconnect = true + } if wantConfigRetryCount < maxWantConfigRetries { Logger.mesh.warning("⏰ Want Config timeout, retrying... (attempt \(self.wantConfigRetryCount + 1)/\(self.maxWantConfigRetries))") sendWantConfig() } else { Logger.mesh.error("🚨 Want Config failed after \(self.maxWantConfigRetries) attempts, forcing disconnect") lastConnectionError = "Bluetooth connection timeout, keep your node closer or reboot your radio if the problem continues.".localized - allowDisconnect = true + disconnectPeripheral(reconnect: false) } } diff --git a/Meshtastic/Views/Bluetooth/Connect.swift b/Meshtastic/Views/Bluetooth/Connect.swift index 1176241a..e22e844a 100644 --- a/Meshtastic/Views/Bluetooth/Connect.swift +++ b/Meshtastic/Views/Bluetooth/Connect.swift @@ -282,7 +282,7 @@ struct Connect: View { .controlSize(.large) .padding() } - if bleManager.isConnecting { + if bleManager.allowDisconnect { Button(role: .destructive, action: { bleManager.cancelPeripheralConnection() From 7edd9764d966b2419ed473093738bb0c3ba15948 Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Tue, 17 Jun 2025 23:17:22 -0700 Subject: [PATCH 141/213] 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 6f72607e..244ee935 100644 --- a/Meshtastic.xcodeproj/project.pbxproj +++ b/Meshtastic.xcodeproj/project.pbxproj @@ -1808,7 +1808,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 2.6.7; + MARKETING_VERSION = 2.6.8; PRODUCT_BUNDLE_IDENTIFIER = gvh.MeshtasticClient; PRODUCT_NAME = "$(TARGET_NAME)"; SUPPORTS_MACCATALYST = YES; @@ -1841,7 +1841,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 2.6.7; + MARKETING_VERSION = 2.6.8; PRODUCT_BUNDLE_IDENTIFIER = gvh.MeshtasticClient; PRODUCT_NAME = "$(TARGET_NAME)"; SUPPORTS_MACCATALYST = YES; @@ -1872,7 +1872,7 @@ "@executable_path/Frameworks", "@executable_path/../../Frameworks", ); - MARKETING_VERSION = 2.6.7; + MARKETING_VERSION = 2.6.8; PRODUCT_BUNDLE_IDENTIFIER = gvh.MeshtasticClient.Widgets; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; @@ -1904,7 +1904,7 @@ "@executable_path/Frameworks", "@executable_path/../../Frameworks", ); - MARKETING_VERSION = 2.6.7; + MARKETING_VERSION = 2.6.8; PRODUCT_BUNDLE_IDENTIFIER = gvh.MeshtasticClient.Widgets; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; From 24e7974f6e4f272b8486e3af494fcd3afb554974 Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Wed, 18 Jun 2025 01:00:27 -0700 Subject: [PATCH 142/213] iCloud Private key backup initial commit --- Localizable.xcstrings | 23 ++++ Meshtastic.xcodeproj/project.pbxproj | 4 + Meshtastic/Helpers/KeychainHelper.swift | 66 ++++++++++ Meshtastic/Meshtastic.entitlements | 12 +- .../Settings/Config/SecurityConfig.swift | 122 +++++++++++++----- 5 files changed, 191 insertions(+), 36 deletions(-) create mode 100644 Meshtastic/Helpers/KeychainHelper.swift diff --git a/Localizable.xcstrings b/Localizable.xcstrings index 9a6c28c1..b0443c35 100644 --- a/Localizable.xcstrings +++ b/Localizable.xcstrings @@ -2251,6 +2251,7 @@ } }, "Admin & Direct Message Keys" : { + "extractionState" : "stale", "localizations" : { "de" : { "stringUnit" : { @@ -2283,6 +2284,9 @@ } } } + }, + "Admin Keys" : { + }, "Administration" : { "localizations" : { @@ -3777,6 +3781,12 @@ } } } + }, + "Backup" : { + + }, + "Backup your private key to your iCloud keychain." : { + }, "Bad" : { "localizations" : { @@ -9373,6 +9383,9 @@ } } } + }, + "Direct Message Key" : { + }, "Direct Messages" : { "localizations" : { @@ -13128,6 +13141,9 @@ } } } + }, + "Generated from your public key and sent out to other nodes on the mesh to allow them to compute a shared secret key." : { + }, "Get custom waterproof solar and detection sensor router nodes, aluminium desktop nodes and rugged handsets." : { "localizations" : { @@ -15510,6 +15526,9 @@ } } } + }, + "Key Backup" : { + }, "Key Mapping" : { "localizations" : { @@ -24541,6 +24560,9 @@ } } } + }, + "Restore" : { + }, "Resume" : { "localizations" : { @@ -27216,6 +27238,7 @@ } }, "Sent out to other nodes on the mesh to allow them to compute a shared secret key." : { + "extractionState" : "stale", "localizations" : { "de" : { "stringUnit" : { diff --git a/Meshtastic.xcodeproj/project.pbxproj b/Meshtastic.xcodeproj/project.pbxproj index 244ee935..d4ea70bc 100644 --- a/Meshtastic.xcodeproj/project.pbxproj +++ b/Meshtastic.xcodeproj/project.pbxproj @@ -97,6 +97,7 @@ DD1BD0EB2C601795008C0C70 /* CLLocation.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD1BD0EA2C601795008C0C70 /* CLLocation.swift */; }; DD1BD0EE2C603C91008C0C70 /* CustomFormatters.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD1BD0ED2C603C91008C0C70 /* CustomFormatters.swift */; }; DD1BD0F32C63C65E008C0C70 /* SecurityConfig.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD1BD0F22C63C65E008C0C70 /* SecurityConfig.swift */; }; + DD1BEF4A2E0292320090CE24 /* KeychainHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD1BEF492E0292220090CE24 /* KeychainHelper.swift */; }; DD1BF2F92776FE2E008C8D2F /* UserMessageList.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD1BF2F82776FE2E008C8D2F /* UserMessageList.swift */; }; DD2160AF28C5552500C17253 /* MQTTConfig.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD2160AE28C5552500C17253 /* MQTTConfig.swift */; }; DD23A50F26FD1B4400D9B90C /* PeripheralModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD23A50E26FD1B4400D9B90C /* PeripheralModel.swift */; }; @@ -372,6 +373,7 @@ DD1BD0F12C61D3AD008C0C70 /* MeshtasticDataModelV 42.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = "MeshtasticDataModelV 42.xcdatamodel"; sourceTree = ""; }; DD1BD0F22C63C65E008C0C70 /* SecurityConfig.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SecurityConfig.swift; sourceTree = ""; }; DD1BEF462DFF284C0090CE24 /* MeshtasticDataModelV 53.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = "MeshtasticDataModelV 53.xcdatamodel"; sourceTree = ""; }; + DD1BEF492E0292220090CE24 /* KeychainHelper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeychainHelper.swift; sourceTree = ""; }; DD1BF2F82776FE2E008C8D2F /* UserMessageList.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserMessageList.swift; sourceTree = ""; }; DD2160AE28C5552500C17253 /* MQTTConfig.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MQTTConfig.swift; sourceTree = ""; }; DD23A50E26FD1B4400D9B90C /* PeripheralModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PeripheralModel.swift; sourceTree = ""; }; @@ -1067,6 +1069,7 @@ children = ( DDD43FE12A78C86B0083A3E9 /* Mqtt */, DDAF8C5226EB1DF10058C060 /* BLEManager.swift */, + DD1BEF492E0292220090CE24 /* KeychainHelper.swift */, DDC2E1A626CEB3400042C5E4 /* LocationHelper.swift */, DD913638270DFF4C00D7ACF3 /* LocalNotificationManager.swift */, DDA6B2E828419CF2003E8C16 /* MeshPackets.swift */, @@ -1576,6 +1579,7 @@ 233E99C72D84A70900CC3A77 /* SoilCompactWidgets.swift in Sources */, BCE2D3C92C7C377F008E6199 /* FactoryResetNodeIntent.swift in Sources */, DD93800B2BA3F968008BEC06 /* NodeMapContent.swift in Sources */, + DD1BEF4A2E0292320090CE24 /* KeychainHelper.swift in Sources */, DD41582A28585C32009B0E59 /* RangeTestConfig.swift in Sources */, DD1925B728CDA5A400720036 /* CannedMessagesConfigEnums.swift in Sources */, DDDB444429F8A8DD00EE2349 /* Float.swift in Sources */, diff --git a/Meshtastic/Helpers/KeychainHelper.swift b/Meshtastic/Helpers/KeychainHelper.swift new file mode 100644 index 00000000..73242474 --- /dev/null +++ b/Meshtastic/Helpers/KeychainHelper.swift @@ -0,0 +1,66 @@ +// +// KeychainHelper.swift +// Meshtastic +// +// Copyright(c) Garth Vander Houwen 6/17/25. +// +import Foundation +import Security + +class KeychainHelper { + + static let standard = KeychainHelper() + + private init() {} + + func save(key: String, value: String, service: String = Bundle.main.bundleIdentifier!) -> OSStatus { + let data = value.data(using: .utf8)! + + let query: [String: Any] = [ + kSecClass as String: kSecClassGenericPassword, + kSecAttrService as String: service, + kSecAttrAccount as String: key, + kSecValueData as String: data, + kSecAttrSynchronizable as String: kCFBooleanTrue!, + kSecAttrAccessible as String: kSecAttrAccessibleWhenUnlocked + ] + + SecItemDelete(query as CFDictionary) // Delete existing item if any + + let status = SecItemAdd(query as CFDictionary, nil) + return status + } + + func read(key: String, service: String = Bundle.main.bundleIdentifier!) -> String? { + let query: [String: Any] = [ + kSecClass as String: kSecClassGenericPassword, + kSecAttrService as String: service, + kSecAttrAccount as String: key, + kSecReturnData as String: kCFBooleanTrue, + kSecMatchLimit as String: kSecMatchLimitOne, + kSecAttrSynchronizable as String: kCFBooleanTrue! + ] + + var item: CFTypeRef? + let status = SecItemCopyMatching(query as CFDictionary, &item) + + if status == errSecSuccess { + if let data = item as? Data { + return String(data: data, encoding: .utf8) + } + } + return nil + } + + func delete(key: String, service: String = Bundle.main.bundleIdentifier!) -> OSStatus { + let query: [String: Any] = [ + kSecClass as String: kSecClassGenericPassword, + kSecAttrService as String: service, + kSecAttrAccount as String: key, + kSecAttrSynchronizable as String: kCFBooleanTrue! + ] + + let status = SecItemDelete(query as CFDictionary) + return status + } +} diff --git a/Meshtastic/Meshtastic.entitlements b/Meshtastic/Meshtastic.entitlements index 0d2247ee..3689ef3e 100644 --- a/Meshtastic/Meshtastic.entitlements +++ b/Meshtastic/Meshtastic.entitlements @@ -2,13 +2,15 @@ - com.apple.developer.usernotifications.critical-alerts - com.apple.developer.associated-domains applinks:meshtastic.org/e/* applinks:meshtastic.org/v/* + com.apple.developer.carplay-communication + + com.apple.developer.usernotifications.critical-alerts + com.apple.developer.weatherkit com.apple.security.app-sandbox @@ -21,7 +23,9 @@ com.apple.security.personal-information.location - com.apple.developer.carplay-communication - + keychain-access-groups + + $(AppIdentifierPrefix)gvh.MeshtasticClient + diff --git a/Meshtastic/Views/Settings/Config/SecurityConfig.swift b/Meshtastic/Views/Settings/Config/SecurityConfig.swift index 5954f47d..dc2d62b8 100644 --- a/Meshtastic/Views/Settings/Config/SecurityConfig.swift +++ b/Meshtastic/Views/Settings/Config/SecurityConfig.swift @@ -51,7 +51,7 @@ struct SecurityConfig: View { ConfigHeader(title: "Security", config: \.securityConfig, node: node, onAppear: setSecurityValues) Text("Security Config Settings require a firmware version 2.5+") .font(.title3) - Section(header: Text("Admin & Direct Message Keys")) { + Section(header: Text("Direct Message Key")) { VStack(alignment: .leading) { Label("Public Key", systemImage: "key") Text(publicKey) @@ -66,7 +66,7 @@ struct SecurityConfig: View { RoundedRectangle(cornerRadius: 10.0) .stroke(isValidKeyPair ? Color.clear : Color.red, lineWidth: 2.0) ) - Text("Sent out to other nodes on the mesh to allow them to compute a shared secret key.") + Text("Generated from your public key and sent out to other nodes on the mesh to allow them to compute a shared secret key.") .foregroundStyle(.secondary) .font(idiom == .phone ? .caption : .callout) Divider() @@ -79,6 +79,63 @@ struct SecurityConfig: View { Text("Used to create a shared key with a remote device.") .foregroundStyle(.secondary) .font(idiom == .phone ? .caption : .callout) + if let currentNode = node { + Divider() + Label("Key Backup", systemImage: "icloud") + HStack(alignment: .firstTextBaseline) { + let keychainKey = "PrivateKeyNode\(currentNode.num)" + Button { + let status = KeychainHelper.standard.save(key: keychainKey, value: privateKey) + if status == errSecSuccess { + print("Value saved successfully!") + } else { + print("Error saving value: \(status)") + } + } + label: { + Image(systemName: "icloud.and.arrow.up") + Text("Backup") + } + .buttonStyle(.bordered) + .buttonBorderShape(.capsule) + .controlSize(.small) + Spacer() + Button { + if let value = KeychainHelper.standard.read(key: keychainKey) { + self.privateKey = value + self.privateKeyIsSecure = false + } else { + print("No value found in Keychain for key: \(keychainKey)") + } + } + label: { + Image(systemName: "key.icloud") + Text("Restore") + } + .buttonStyle(.bordered) + .buttonBorderShape(.capsule) + .controlSize(.small) + Spacer() + Button { + let status = KeychainHelper.standard.delete(key: keychainKey) + if status == errSecSuccess { + print("Value deleted successfully!") + } else { + print("Error deleting value: \(status)") + } + } + label: { + Image(systemName: "trash") + } + .buttonStyle(.bordered) + .buttonBorderShape(.capsule) + .controlSize(.small) + } + Text("Backup your private key to your iCloud keychain.") + .foregroundStyle(.secondary) + .font(idiom == .phone ? .caption : .callout) + Divider() + } HStack(alignment: .firstTextBaseline) { Label("Regenerate Private Key", systemImage: "arrow.clockwise.circle") Spacer() @@ -95,38 +152,39 @@ struct SecurityConfig: View { .buttonBorderShape(.capsule) .controlSize(.small) } - Divider() - Label("Primary Admin Key", systemImage: "key.viewfinder") - SecureInput("Primary Admin Key", text: $adminKey, isValid: $hasValidAdminKey) - .background( - RoundedRectangle(cornerRadius: 10.0) - .stroke(hasValidAdminKey ? Color.clear : Color.red, lineWidth: 2.0) - ) - Text("The primary public key authorized to send admin messages to this node.") - .foregroundStyle(.secondary) - .font(idiom == .phone ? .caption : .callout) - Divider() - Label("Secondary Admin Key", systemImage: "key.viewfinder") - SecureInput("Secondary Admin Key", text: $adminKey2, isValid: $hasValidAdminKey2) - .background( - RoundedRectangle(cornerRadius: 10.0) - .stroke(hasValidAdminKey2 ? Color.clear : Color.red, lineWidth: 2.0) - ) - Text("The secondary public key authorized to send admin messages to this node.") - .foregroundStyle(.secondary) - .font(idiom == .phone ? .caption : .callout) - Divider() - Label("Tertiary Admin Key", systemImage: "key.viewfinder") - SecureInput("Tertiary Admin Key", text: $adminKey3, isValid: $hasValidAdminKey3) - .background( - RoundedRectangle(cornerRadius: 10.0) - .stroke(hasValidAdminKey3 ? Color.clear : Color.red, lineWidth: 2.0) - ) - Text("The tertiary public key authorized to send admin messages to this node.") - .foregroundStyle(.secondary) - .font(idiom == .phone ? .caption : .callout) } } + Section(header: Text("Admin Keys")) { + Label("Primary Admin Key", systemImage: "key.viewfinder") + SecureInput("Primary Admin Key", text: $adminKey, isValid: $hasValidAdminKey) + .background( + RoundedRectangle(cornerRadius: 10.0) + .stroke(hasValidAdminKey ? Color.clear : Color.red, lineWidth: 2.0) + ) + Text("The primary public key authorized to send admin messages to this node.") + .foregroundStyle(.secondary) + .font(idiom == .phone ? .caption : .callout) + Divider() + Label("Secondary Admin Key", systemImage: "key.viewfinder") + SecureInput("Secondary Admin Key", text: $adminKey2, isValid: $hasValidAdminKey2) + .background( + RoundedRectangle(cornerRadius: 10.0) + .stroke(hasValidAdminKey2 ? Color.clear : Color.red, lineWidth: 2.0) + ) + Text("The secondary public key authorized to send admin messages to this node.") + .foregroundStyle(.secondary) + .font(idiom == .phone ? .caption : .callout) + Divider() + Label("Tertiary Admin Key", systemImage: "key.viewfinder") + SecureInput("Tertiary Admin Key", text: $adminKey3, isValid: $hasValidAdminKey3) + .background( + RoundedRectangle(cornerRadius: 10.0) + .stroke(hasValidAdminKey3 ? Color.clear : Color.red, lineWidth: 2.0) + ) + Text("The tertiary public key authorized to send admin messages to this node.") + .foregroundStyle(.secondary) + .font(idiom == .phone ? .caption : .callout) + } Section(header: Text("Logs")) { Toggle(isOn: $serialEnabled) { Label("Serial Console", systemImage: "terminal") From f1b371d0b7ba09bc7148bb52ec3c4d3abe79b5cc Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Wed, 18 Jun 2025 07:26:56 -0700 Subject: [PATCH 143/213] revert --- Meshtastic/Views/Bluetooth/Connect.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Meshtastic/Views/Bluetooth/Connect.swift b/Meshtastic/Views/Bluetooth/Connect.swift index e22e844a..1176241a 100644 --- a/Meshtastic/Views/Bluetooth/Connect.swift +++ b/Meshtastic/Views/Bluetooth/Connect.swift @@ -282,7 +282,7 @@ struct Connect: View { .controlSize(.large) .padding() } - if bleManager.allowDisconnect { + if bleManager.isConnecting { Button(role: .destructive, action: { bleManager.cancelPeripheralConnection() From 2d8ede1c7d00f9b7095ad87496f44a4107a6ba12 Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Wed, 18 Jun 2025 08:44:13 -0700 Subject: [PATCH 144/213] Success and Error states for key backup --- Localizable.xcstrings | 3 ++ Meshtastic.xcodeproj/project.pbxproj | 4 ++ Meshtastic/Enums/KeyBackupStatus.swift | 47 +++++++++++++++++++ .../Settings/Config/SecurityConfig.swift | 23 ++++++--- 4 files changed, 71 insertions(+), 6 deletions(-) create mode 100644 Meshtastic/Enums/KeyBackupStatus.swift diff --git a/Localizable.xcstrings b/Localizable.xcstrings index b0443c35..2676148d 100644 --- a/Localizable.xcstrings +++ b/Localizable.xcstrings @@ -13083,6 +13083,9 @@ } } } + }, + "Generate a new private key to replace the one currently in use. Public key will automatically be regenerated as well." : { + }, "Generate QR Code" : { "localizations" : { diff --git a/Meshtastic.xcodeproj/project.pbxproj b/Meshtastic.xcodeproj/project.pbxproj index d4ea70bc..745c3602 100644 --- a/Meshtastic.xcodeproj/project.pbxproj +++ b/Meshtastic.xcodeproj/project.pbxproj @@ -98,6 +98,7 @@ DD1BD0EE2C603C91008C0C70 /* CustomFormatters.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD1BD0ED2C603C91008C0C70 /* CustomFormatters.swift */; }; DD1BD0F32C63C65E008C0C70 /* SecurityConfig.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD1BD0F22C63C65E008C0C70 /* SecurityConfig.swift */; }; DD1BEF4A2E0292320090CE24 /* KeychainHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD1BEF492E0292220090CE24 /* KeychainHelper.swift */; }; + DD1BEF4C2E030D310090CE24 /* KeyBackupStatus.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD1BEF4B2E030D240090CE24 /* KeyBackupStatus.swift */; }; DD1BF2F92776FE2E008C8D2F /* UserMessageList.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD1BF2F82776FE2E008C8D2F /* UserMessageList.swift */; }; DD2160AF28C5552500C17253 /* MQTTConfig.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD2160AE28C5552500C17253 /* MQTTConfig.swift */; }; DD23A50F26FD1B4400D9B90C /* PeripheralModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD23A50E26FD1B4400D9B90C /* PeripheralModel.swift */; }; @@ -374,6 +375,7 @@ DD1BD0F22C63C65E008C0C70 /* SecurityConfig.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SecurityConfig.swift; sourceTree = ""; }; DD1BEF462DFF284C0090CE24 /* MeshtasticDataModelV 53.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = "MeshtasticDataModelV 53.xcdatamodel"; sourceTree = ""; }; DD1BEF492E0292220090CE24 /* KeychainHelper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeychainHelper.swift; sourceTree = ""; }; + DD1BEF4B2E030D240090CE24 /* KeyBackupStatus.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeyBackupStatus.swift; sourceTree = ""; }; DD1BF2F82776FE2E008C8D2F /* UserMessageList.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserMessageList.swift; sourceTree = ""; }; DD2160AE28C5552500C17253 /* MQTTConfig.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MQTTConfig.swift; sourceTree = ""; }; DD23A50E26FD1B4400D9B90C /* PeripheralModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PeripheralModel.swift; sourceTree = ""; }; @@ -878,6 +880,7 @@ DD8ED9C6289CE4A100B3B0AB /* Enums */ = { isa = PBXGroup; children = ( + DD1BEF4B2E030D240090CE24 /* KeyBackupStatus.swift */, DDA951592BC6624100CEA535 /* TelemetryWeather.swift */, DDE0F7C4295F77B700B8AAB3 /* AppSettingsEnums.swift */, DDB6ABD828B0A4BA00384BA1 /* BluetoothModes.swift */, @@ -1446,6 +1449,7 @@ 233E99C32D849D7A00CC3A77 /* WeightCompactWidget.swift in Sources */, DD4A911E2708C65400501B7E /* AppSettings.swift in Sources */, DD1BD0F32C63C65E008C0C70 /* SecurityConfig.swift in Sources */, + DD1BEF4C2E030D310090CE24 /* KeyBackupStatus.swift in Sources */, DD2160AF28C5552500C17253 /* MQTTConfig.swift in Sources */, DD6F65722C6AB8EC0053C113 /* SecureInput.swift in Sources */, DD13AA492AB73BF400BA0C98 /* PositionPopover.swift in Sources */, diff --git a/Meshtastic/Enums/KeyBackupStatus.swift b/Meshtastic/Enums/KeyBackupStatus.swift new file mode 100644 index 00000000..ff5b2438 --- /dev/null +++ b/Meshtastic/Enums/KeyBackupStatus.swift @@ -0,0 +1,47 @@ +// +// iCloudStats.swift +// Meshtastic +// +// Copyright(c) Garth Vander Houwen 6/18/25. +// + +enum KeyBackupStatus: String, CaseIterable, Equatable, Decodable { + case saved + case restored + case deleted + case saveFailed + case restoreFailed + case deleteFailed + var description: String { + switch self { + case .saved: + return "Private Key saved successfully to iCloud keychain.".localized + case .restored: + return "Private Key restored successfully from iCloud keychain.".localized + case .deleted: + return "Private Key deleted successfully from iCloud keychain.".localized + case .saveFailed: + return "Private Key failed to save to iCloud keychain.".localized + case .restoreFailed: + return "Private Key value not found in iCloud keychain.".localized + case .deleteFailed: + return "Private Key failed to delete from iCloud keychain.".localized + } + } + var success: Bool { + switch self { + case .saved: + return true + case .restored: + return true + case .deleted: + return true + case .saveFailed: + return false + case .restoreFailed: + return false + case .deleteFailed: + return false + } + } +} diff --git a/Meshtastic/Views/Settings/Config/SecurityConfig.swift b/Meshtastic/Views/Settings/Config/SecurityConfig.swift index dc2d62b8..52a72e93 100644 --- a/Meshtastic/Views/Settings/Config/SecurityConfig.swift +++ b/Meshtastic/Views/Settings/Config/SecurityConfig.swift @@ -35,6 +35,8 @@ struct SecurityConfig: View { @State var serialEnabled = false @State var debugLogApiEnabled = false @State var privateKeyIsSecure = true + @State var backupStatus: KeyBackupStatus? + @State var backupStatusError: OSStatus? private var isValidKeyPair: Bool { guard let privateKeyBytes = Data(base64Encoded: privateKey), @@ -87,9 +89,10 @@ struct SecurityConfig: View { Button { let status = KeychainHelper.standard.save(key: keychainKey, value: privateKey) if status == errSecSuccess { - print("Value saved successfully!") + backupStatus = KeyBackupStatus.saved } else { - print("Error saving value: \(status)") + backupStatus = KeyBackupStatus.saveFailed + backupStatusError = status } } label: { @@ -104,8 +107,9 @@ struct SecurityConfig: View { if let value = KeychainHelper.standard.read(key: keychainKey) { self.privateKey = value self.privateKeyIsSecure = false + backupStatus = KeyBackupStatus.restored } else { - print("No value found in Keychain for key: \(keychainKey)") + backupStatus = KeyBackupStatus.restoreFailed } } label: { @@ -119,9 +123,9 @@ struct SecurityConfig: View { Button { let status = KeychainHelper.standard.delete(key: keychainKey) if status == errSecSuccess { - print("Value deleted successfully!") + backupStatus = KeyBackupStatus.deleted } else { - print("Error deleting value: \(status)") + backupStatus = KeyBackupStatus.deleteFailed } } label: { @@ -131,11 +135,17 @@ struct SecurityConfig: View { .buttonBorderShape(.capsule) .controlSize(.small) } + if let status = backupStatus { + let state = status.success + Text("\(status.description)") + .font(.caption) + .foregroundColor(state ? .green : .red) + } Text("Backup your private key to your iCloud keychain.") .foregroundStyle(.secondary) .font(idiom == .phone ? .caption : .callout) - Divider() } + Divider() HStack(alignment: .firstTextBaseline) { Label("Regenerate Private Key", systemImage: "arrow.clockwise.circle") Spacer() @@ -152,6 +162,7 @@ struct SecurityConfig: View { .buttonBorderShape(.capsule) .controlSize(.small) } + Text("Generate a new private key to replace the one currently in use. Public key will automatically be regenerated as well.") } } Section(header: Text("Admin Keys")) { From e0678258be2d14bb7a8b14d6184501f486de8fc8 Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Wed, 18 Jun 2025 16:44:21 -0700 Subject: [PATCH 145/213] Adjust key generation, clean up userid's --- Meshtastic.xcodeproj/project.pbxproj | 8 ++++---- Meshtastic/Extensions/CoreData/UserEntityExtension.swift | 6 ++---- Meshtastic/Helpers/MeshPackets.swift | 4 ++-- Meshtastic/Persistence/UpdateCoreData.swift | 4 ++-- Meshtastic/Views/Nodes/Helpers/NodeDetail.swift | 2 +- Meshtastic/Views/Settings/Config/SecurityConfig.swift | 9 ++++++++- Meshtastic/Views/Settings/ShareChannels.swift | 2 +- 7 files changed, 20 insertions(+), 15 deletions(-) diff --git a/Meshtastic.xcodeproj/project.pbxproj b/Meshtastic.xcodeproj/project.pbxproj index 6f72607e..244ee935 100644 --- a/Meshtastic.xcodeproj/project.pbxproj +++ b/Meshtastic.xcodeproj/project.pbxproj @@ -1808,7 +1808,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 2.6.7; + MARKETING_VERSION = 2.6.8; PRODUCT_BUNDLE_IDENTIFIER = gvh.MeshtasticClient; PRODUCT_NAME = "$(TARGET_NAME)"; SUPPORTS_MACCATALYST = YES; @@ -1841,7 +1841,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 2.6.7; + MARKETING_VERSION = 2.6.8; PRODUCT_BUNDLE_IDENTIFIER = gvh.MeshtasticClient; PRODUCT_NAME = "$(TARGET_NAME)"; SUPPORTS_MACCATALYST = YES; @@ -1872,7 +1872,7 @@ "@executable_path/Frameworks", "@executable_path/../../Frameworks", ); - MARKETING_VERSION = 2.6.7; + MARKETING_VERSION = 2.6.8; PRODUCT_BUNDLE_IDENTIFIER = gvh.MeshtasticClient.Widgets; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; @@ -1904,7 +1904,7 @@ "@executable_path/Frameworks", "@executable_path/../../Frameworks", ); - MARKETING_VERSION = 2.6.7; + MARKETING_VERSION = 2.6.8; PRODUCT_BUNDLE_IDENTIFIER = gvh.MeshtasticClient.Widgets; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; diff --git a/Meshtastic/Extensions/CoreData/UserEntityExtension.swift b/Meshtastic/Extensions/CoreData/UserEntityExtension.swift index 92f097b3..14bc4948 100644 --- a/Meshtastic/Extensions/CoreData/UserEntityExtension.swift +++ b/Meshtastic/Extensions/CoreData/UserEntityExtension.swift @@ -118,10 +118,8 @@ public func createUser(num: Int64, context: NSManagedObjectContext) throws -> Us context.performAndWait { newUser = UserEntity(context: context) newUser.num = num - - let userId = String(format: "%016llX", num) - newUser.userId = "!\(userId)" - + let userId = num.toHex() + newUser.userId = userId let last4 = String(userId.suffix(4)) newUser.longName = "Meshtastic \(last4)" newUser.shortName = last4 diff --git a/Meshtastic/Helpers/MeshPackets.swift b/Meshtastic/Helpers/MeshPackets.swift index 75a62864..1e97611b 100644 --- a/Meshtastic/Helpers/MeshPackets.swift +++ b/Meshtastic/Helpers/MeshPackets.swift @@ -296,7 +296,7 @@ func nodeInfoPacket (nodeInfo: NodeInfo, channel: UInt32, context: NSManagedObje if nodeInfo.hasUser { let newUser = UserEntity(context: context) - newUser.userId = nodeInfo.user.id + newUser.userId = nodeInfo.num.toHex() newUser.num = Int64(nodeInfo.num) newUser.longName = nodeInfo.user.longName newUser.shortName = nodeInfo.user.shortName @@ -394,7 +394,7 @@ func nodeInfoPacket (nodeInfo: NodeInfo, channel: UInt32, context: NSManagedObje fetchedNode[0].user?.pkiEncrypted = true fetchedNode[0].user?.publicKey = nodeInfo.user.publicKey } - fetchedNode[0].user?.userId = nodeInfo.user.id + fetchedNode[0].user?.userId = nodeInfo.num.toHex() fetchedNode[0].user?.num = Int64(nodeInfo.num) fetchedNode[0].user?.numString = String(nodeInfo.num) fetchedNode[0].user?.longName = nodeInfo.user.longName diff --git a/Meshtastic/Persistence/UpdateCoreData.swift b/Meshtastic/Persistence/UpdateCoreData.swift index c241831a..78b14ef7 100644 --- a/Meshtastic/Persistence/UpdateCoreData.swift +++ b/Meshtastic/Persistence/UpdateCoreData.swift @@ -182,7 +182,7 @@ func upsertNodeInfoPacket (packet: MeshPacket, context: NSManagedObjectContext) } else { let newUser = UserEntity(context: context) - newUser.userId = newUserMessage.id + newUser.userId = newNode.num.toHex() newUser.num = Int64(packet.from) newUser.longName = newUserMessage.longName newUser.shortName = newUserMessage.shortName @@ -306,7 +306,7 @@ func upsertNodeInfoPacket (packet: MeshPacket, context: NSManagedObjectContext) fetchedNode[0].telemetries? = NSOrderedSet(array: newTelemetries) } if nodeInfoMessage.hasUser { - fetchedNode[0].user?.userId = nodeInfoMessage.user.id + fetchedNode[0].user?.userId = nodeInfoMessage.num.toHex() fetchedNode[0].user?.num = Int64(nodeInfoMessage.num) fetchedNode[0].user?.longName = nodeInfoMessage.user.longName fetchedNode[0].user?.shortName = nodeInfoMessage.user.shortName diff --git a/Meshtastic/Views/Nodes/Helpers/NodeDetail.swift b/Meshtastic/Views/Nodes/Helpers/NodeDetail.swift index 80d10839..d98941bb 100644 --- a/Meshtastic/Views/Nodes/Helpers/NodeDetail.swift +++ b/Meshtastic/Views/Nodes/Helpers/NodeDetail.swift @@ -122,7 +122,7 @@ struct NodeDetail: View { .textSelection(.enabled) } .accessibilityElement(children: .combine) - + if node.user?.keyMatch ?? false { if let publicKey = node.user?.publicKey { HStack { diff --git a/Meshtastic/Views/Settings/Config/SecurityConfig.swift b/Meshtastic/Views/Settings/Config/SecurityConfig.swift index 5954f47d..006b8fa0 100644 --- a/Meshtastic/Views/Settings/Config/SecurityConfig.swift +++ b/Meshtastic/Views/Settings/Config/SecurityConfig.swift @@ -305,7 +305,14 @@ struct SecurityConfig: View { } if status == errSecSuccess { - return randomBytes + // Generate a random "f" value and then adjust the value to make + // it valid as an "s" value for eval(). According to the specification + // we need to mask off the 3 right-most bits of f[0], mask off the + // left-most bit of f[31], and set the second to left-most bit of f[31]. + var f = randomBytes + f[0] &= 0xF8 + f[31] = (f[31] & 0x7F) | 0x40 + return f } else { // Handle error, perhaps by logging or throwing an exception Logger.mesh.debug("Error generating random bytes: \(status)") diff --git a/Meshtastic/Views/Settings/ShareChannels.swift b/Meshtastic/Views/Settings/ShareChannels.swift index d1c09deb..1fd10c81 100644 --- a/Meshtastic/Views/Settings/ShareChannels.swift +++ b/Meshtastic/Views/Settings/ShareChannels.swift @@ -313,7 +313,7 @@ struct ShareChannels: View { guard let settingsString = try? channelSet.serializedData().base64EncodedString() else { return } - channelsUrl = ("https://meshtastic.org/e/\(replaceChannels ? "" : "?add=true")#" + settingsString.base64ToBase64url()) + channelsUrl = ("https://meshtastic.org/e/#\(settingsString.base64ToBase64url())\(replaceChannels ? "" : "?add=true")") } } } From 097d91059331fb6a5dde9fe62df6657b3ef40008 Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Wed, 18 Jun 2025 17:00:25 -0700 Subject: [PATCH 146/213] Update Meshtastic/Views/Settings/ShareChannels.swift Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- Meshtastic/Views/Settings/ShareChannels.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Meshtastic/Views/Settings/ShareChannels.swift b/Meshtastic/Views/Settings/ShareChannels.swift index 1fd10c81..7e4068e2 100644 --- a/Meshtastic/Views/Settings/ShareChannels.swift +++ b/Meshtastic/Views/Settings/ShareChannels.swift @@ -313,7 +313,7 @@ struct ShareChannels: View { guard let settingsString = try? channelSet.serializedData().base64EncodedString() else { return } - channelsUrl = ("https://meshtastic.org/e/#\(settingsString.base64ToBase64url())\(replaceChannels ? "" : "?add=true")") + channelsUrl = ("https://meshtastic.org/e/\(replaceChannels ? "" : "?add=true")#\(settingsString.base64ToBase64url())") } } } From 17c8ce671d5a74a5a454c32553ac5c61528ea7b5 Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Wed, 18 Jun 2025 17:04:58 -0700 Subject: [PATCH 147/213] Update channel qr code url matching --- Meshtastic/MeshtasticApp.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Meshtastic/MeshtasticApp.swift b/Meshtastic/MeshtasticApp.swift index 685ae640..7cd636ac 100644 --- a/Meshtastic/MeshtasticApp.swift +++ b/Meshtastic/MeshtasticApp.swift @@ -59,7 +59,7 @@ struct MeshtasticAppleApp: App { self.saveChannels = false if self.incomingUrl?.absoluteString.lowercased().contains("meshtastic.org/v/#") == true { handleContactUrl(url: self.incomingUrl!) - } else if self.incomingUrl?.absoluteString.lowercased().contains("meshtastic.org/e/#") == true { + } 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 if (self.incomingUrl?.absoluteString.lowercased().contains("?")) != nil { From ca6cf606bceddd4c542a8d688649b096ec86970f Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Wed, 18 Jun 2025 17:17:34 -0700 Subject: [PATCH 148/213] Fix a second channel url fragment --- Meshtastic/MeshtasticApp.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Meshtastic/MeshtasticApp.swift b/Meshtastic/MeshtasticApp.swift index 7cd636ac..9a0e9165 100644 --- a/Meshtastic/MeshtasticApp.swift +++ b/Meshtastic/MeshtasticApp.swift @@ -87,7 +87,7 @@ struct MeshtasticAppleApp: App { self.incomingUrl = url if url.absoluteString.lowercased().contains("meshtastic.org/v/#") { handleContactUrl(url: url) - } else if url.absoluteString.lowercased().contains("meshtastic.org/e/#") { + } 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 if self.incomingUrl?.absoluteString.lowercased().contains("?") != nil { From 63bc7a5805b28f01ff54ff27f639aae642db656e Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Wed, 18 Jun 2025 17:51:09 -0700 Subject: [PATCH 149/213] Channels help --- Localizable.xcstrings | 12 +++ Meshtastic.xcodeproj/project.pbxproj | 4 + .../Views/Helpers/Help/ChannelsHelp.swift | 76 +++++++++++++++++++ Meshtastic/Views/Messages/ChannelList.swift | 26 ++++++- Meshtastic/Views/Settings/Channels.swift | 7 ++ 5 files changed, 123 insertions(+), 2 deletions(-) create mode 100644 Meshtastic/Views/Helpers/Help/ChannelsHelp.swift diff --git a/Localizable.xcstrings b/Localizable.xcstrings index 2676148d..0a5a5162 100644 --- a/Localizable.xcstrings +++ b/Localizable.xcstrings @@ -1683,6 +1683,12 @@ } } } + }, + "A channel index of 0 indicates the primary channel where all broadcast packets are sent from." : { + + }, + "A green lock means the channel is securely encrypted with either a 128 or 256 bit AES key." : { + }, "A Meshtastic QR code contains the LoRa config and channel values needed for radios to communicate. You can share a complete channel configuration using the Replace Channels option, if you choose Add Channels your shared channels will be added to the channels on the receiving radio." : { "localizations" : { @@ -1741,6 +1747,9 @@ } } } + }, + "A red lock with a slash means the channel is not securely encrypted, it uses either no key at all or a 1 byte known key. Traffic on this channel is easily intercepted." : { + }, "A Trace Route was sent, no response has been received." : { "localizations" : { @@ -6162,6 +6171,9 @@ } } } + }, + "Channels Help" : { + }, "Chart" : { "localizations" : { diff --git a/Meshtastic.xcodeproj/project.pbxproj b/Meshtastic.xcodeproj/project.pbxproj index 745c3602..d583d3df 100644 --- a/Meshtastic.xcodeproj/project.pbxproj +++ b/Meshtastic.xcodeproj/project.pbxproj @@ -99,6 +99,7 @@ DD1BD0F32C63C65E008C0C70 /* SecurityConfig.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD1BD0F22C63C65E008C0C70 /* SecurityConfig.swift */; }; DD1BEF4A2E0292320090CE24 /* KeychainHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD1BEF492E0292220090CE24 /* KeychainHelper.swift */; }; DD1BEF4C2E030D310090CE24 /* KeyBackupStatus.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD1BEF4B2E030D240090CE24 /* KeyBackupStatus.swift */; }; + DD1BEF4E2E03916A0090CE24 /* ChannelsHelp.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD1BEF4D2E0391620090CE24 /* ChannelsHelp.swift */; }; DD1BF2F92776FE2E008C8D2F /* UserMessageList.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD1BF2F82776FE2E008C8D2F /* UserMessageList.swift */; }; DD2160AF28C5552500C17253 /* MQTTConfig.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD2160AE28C5552500C17253 /* MQTTConfig.swift */; }; DD23A50F26FD1B4400D9B90C /* PeripheralModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD23A50E26FD1B4400D9B90C /* PeripheralModel.swift */; }; @@ -376,6 +377,7 @@ DD1BEF462DFF284C0090CE24 /* MeshtasticDataModelV 53.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = "MeshtasticDataModelV 53.xcdatamodel"; sourceTree = ""; }; DD1BEF492E0292220090CE24 /* KeychainHelper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeychainHelper.swift; sourceTree = ""; }; DD1BEF4B2E030D240090CE24 /* KeyBackupStatus.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeyBackupStatus.swift; sourceTree = ""; }; + DD1BEF4D2E0391620090CE24 /* ChannelsHelp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChannelsHelp.swift; sourceTree = ""; }; DD1BF2F82776FE2E008C8D2F /* UserMessageList.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserMessageList.swift; sourceTree = ""; }; DD2160AE28C5552500C17253 /* MQTTConfig.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MQTTConfig.swift; sourceTree = ""; }; DD23A50E26FD1B4400D9B90C /* PeripheralModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PeripheralModel.swift; sourceTree = ""; }; @@ -850,6 +852,7 @@ DD6F65772C6EAB860053C113 /* Help */ = { isa = PBXGroup; children = ( + DD1BEF4D2E0391620090CE24 /* ChannelsHelp.swift */, DD6F65752C6EA5490053C113 /* AckErrors.swift */, DD6F65782C6EADE60053C113 /* DirectMessagesHelp.swift */, DD6F657A2C6EC2900053C113 /* LockLegend.swift */, @@ -1428,6 +1431,7 @@ D9C9839D2B79CFD700BDBE6A /* TextMessageSize.swift in Sources */, DDC94FCE29CF55310082EA6E /* RtttlConfig.swift in Sources */, DD964FBD296E6B01007C176F /* EmojiOnlyTextField.swift in Sources */, + DD1BEF4E2E03916A0090CE24 /* ChannelsHelp.swift in Sources */, DD8169FF272476C700F4AB02 /* LogDocument.swift in Sources */, BCD93CBA2D9E11A2006C9214 /* DisconnectNodeIntent.swift in Sources */, DDC94FC129CE063B0082EA6E /* BatteryLevel.swift in Sources */, diff --git a/Meshtastic/Views/Helpers/Help/ChannelsHelp.swift b/Meshtastic/Views/Helpers/Help/ChannelsHelp.swift new file mode 100644 index 00000000..830fe3cd --- /dev/null +++ b/Meshtastic/Views/Helpers/Help/ChannelsHelp.swift @@ -0,0 +1,76 @@ +// +// ChannelHelp.swift +// Meshtastic +// +// Copyright(c) Garth Vander Houwen 6/18/25. +// + +import SwiftUI + +struct ChannelsHelp: View { + private var idiom: UIUserInterfaceIdiom { UIDevice.current.userInterfaceIdiom } + @Environment(\.dismiss) private var dismiss + + var body: some View { + ScrollView { + Label("Channels Help", systemImage: "questionmark.circle") + .font(.title) + .padding(.vertical) + VStack(alignment: .leading) { + HStack { + CircleText(text: String(0), color: .accentColor) + .brightness(0.2) + .offset(y: -10) + Text("A channel index of 0 indicates the primary channel where all broadcast packets are sent from.") + .fixedSize(horizontal: false, vertical: true) + .padding(.bottom) + } + HStack { + Image(systemName: "lock.fill") + .padding(.bottom) + .foregroundColor(Color.green) + .font(.largeTitle) + Text("A green lock means the channel is securely encrypted with either a 128 or 256 bit AES key.") + .fixedSize(horizontal: false, vertical: true) + .padding(.bottom) + } + HStack { + Image(systemName: "lock.slash.fill") + .padding(.bottom) + .foregroundColor(Color.red) + .font(.largeTitle) + Text("A red lock with a slash means the channel is not securely encrypted, it uses either no key at all or a 1 byte known key. Traffic on this channel is easily intercepted.") + .fixedSize(horizontal: false, vertical: true) + .padding(.bottom) + } + } + +#if targetEnvironment(macCatalyst) + Spacer() + Button { + dismiss() + } label: { + Label("Close", systemImage: "xmark") + } + .buttonStyle(.bordered) + .buttonBorderShape(.capsule) + .controlSize(.large) + .padding(.bottom) +#endif + } + .frame(minHeight: 0, maxHeight: .infinity, alignment: .leading) + .padding() + .presentationDetents([.large]) + .presentationContentInteraction(.scrolls) + .presentationDragIndicator(.visible) + .presentationBackgroundInteraction(.enabled(upThrough: .large)) + } +} + +struct ChannelHelpPreviews: PreviewProvider { + static var previews: some View { + VStack { + ChannelsHelp() + } + } +} diff --git a/Meshtastic/Views/Messages/ChannelList.swift b/Meshtastic/Views/Messages/ChannelList.swift index fec3ab8b..e97dbb47 100644 --- a/Meshtastic/Views/Messages/ChannelList.swift +++ b/Meshtastic/Views/Messages/ChannelList.swift @@ -21,8 +21,8 @@ struct ChannelList: View { var channelSelection: ChannelEntity? @State private var isPresentingDeleteChannelMessagesConfirm: Bool = false - @State private var isPresentingTraceRouteSentAlert = false + @State private var showingHelp = false var restrictedChannels = ["gpio", "mqtt", "serial", "admin"] @@ -168,8 +168,30 @@ struct ChannelList: View { } .padding([.top, .bottom]) .listStyle(.plain) + .navigationTitle("Channels") } } - .navigationTitle("Channels") + .sheet(isPresented: $showingHelp) { + ChannelsHelp() + .presentationDetents([.medium]) + } + .safeAreaInset(edge: .bottom, alignment: .leading) { + HStack { + Button(action: { + withAnimation { + showingHelp = !showingHelp + } + }) { + Image(systemName: !showingHelp ? "questionmark.circle" : "questionmark.circle.fill") + .padding(.vertical, 5) + } + .tint(Color(UIColor.secondarySystemBackground)) + .foregroundColor(.accentColor) + .buttonStyle(.borderedProminent) + } + .controlSize(.regular) + .padding(5) + } + .padding(.bottom, 5) } } diff --git a/Meshtastic/Views/Settings/Channels.swift b/Meshtastic/Views/Settings/Channels.swift index 0a24c8c5..d479403f 100644 --- a/Meshtastic/Views/Settings/Channels.swift +++ b/Meshtastic/Views/Settings/Channels.swift @@ -124,6 +124,13 @@ struct Channels: View { .brightness(0.1) VStack { HStack { + if channel.psk?.hexDescription.count ?? 0 < 3 { + Image(systemName: "lock.slash.fill") + .foregroundColor(.red) + } else { + Image(systemName: "lock.fill") + .foregroundColor(.green) + } if channel.name?.isEmpty ?? false { if channel.role == 1 { Text(String("PrimaryChannel").camelCaseToWords()).font(.headline) From da6447aae5a94c765a3b858ad85fde6916d18c2b Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Wed, 18 Jun 2025 17:55:41 -0700 Subject: [PATCH 150/213] detents --- Meshtastic/Views/Messages/ChannelList.swift | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Meshtastic/Views/Messages/ChannelList.swift b/Meshtastic/Views/Messages/ChannelList.swift index e97dbb47..940396b9 100644 --- a/Meshtastic/Views/Messages/ChannelList.swift +++ b/Meshtastic/Views/Messages/ChannelList.swift @@ -173,7 +173,8 @@ struct ChannelList: View { } .sheet(isPresented: $showingHelp) { ChannelsHelp() - .presentationDetents([.medium]) + .presentationDetents([.medium, .large]) + .presentationDragIndicator(.visible) } .safeAreaInset(edge: .bottom, alignment: .leading) { HStack { From 6980abed5e8428609c958943af7680c8a7b1b4a3 Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Wed, 18 Jun 2025 19:16:56 -0700 Subject: [PATCH 151/213] 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 d583d3df..883f3571 100644 --- a/Meshtastic.xcodeproj/project.pbxproj +++ b/Meshtastic.xcodeproj/project.pbxproj @@ -1820,7 +1820,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 2.6.8; + MARKETING_VERSION = 2.6.9; PRODUCT_BUNDLE_IDENTIFIER = gvh.MeshtasticClient; PRODUCT_NAME = "$(TARGET_NAME)"; SUPPORTS_MACCATALYST = YES; @@ -1853,7 +1853,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 2.6.8; + MARKETING_VERSION = 2.6.9; PRODUCT_BUNDLE_IDENTIFIER = gvh.MeshtasticClient; PRODUCT_NAME = "$(TARGET_NAME)"; SUPPORTS_MACCATALYST = YES; @@ -1884,7 +1884,7 @@ "@executable_path/Frameworks", "@executable_path/../../Frameworks", ); - MARKETING_VERSION = 2.6.8; + MARKETING_VERSION = 2.6.9; PRODUCT_BUNDLE_IDENTIFIER = gvh.MeshtasticClient.Widgets; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; @@ -1916,7 +1916,7 @@ "@executable_path/Frameworks", "@executable_path/../../Frameworks", ); - MARKETING_VERSION = 2.6.8; + MARKETING_VERSION = 2.6.9; PRODUCT_BUNDLE_IDENTIFIER = gvh.MeshtasticClient.Widgets; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; From 6ed183d155b49b0877215118f6c9c616913ef18b Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Thu, 19 Jun 2025 15:40:19 -0700 Subject: [PATCH 152/213] Make clear function return a boolean so wantconfig can be called if connected, don't purge PKI nodes faster than 7 days to ensure proper reporting of key mismatch errors. --- Meshtastic/Helpers/BLEManager.swift | 8 ++++++-- Meshtastic/Persistence/UpdateCoreData.swift | 16 +++++++++++----- 2 files changed, 17 insertions(+), 7 deletions(-) diff --git a/Meshtastic/Helpers/BLEManager.swift b/Meshtastic/Helpers/BLEManager.swift index 9b044fcf..e422a13f 100644 --- a/Meshtastic/Helpers/BLEManager.swift +++ b/Meshtastic/Helpers/BLEManager.swift @@ -88,8 +88,12 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate centralManager = CBCentralManager(delegate: self, queue: nil) mqttManager.delegate = self // Run clearStaleNodes every 10 minutes - maintenenceTimer = Timer.scheduledTimer(withTimeInterval: 600, repeats: true, block: { _ in - clearStaleNodes(nodeExpireDays: Int(self.purgeStaleNodeDays), context: self.context) + maintenenceTimer = Timer.scheduledTimer(withTimeInterval: 60, repeats: true, block: { _ in + let result = clearStaleNodes(nodeExpireDays: Int(self.purgeStaleNodeDays), context: self.context) + // If you are connected and the clear worked, pull nodes back from the node in case we have deleted anything from that app that is in the device nodedb + if result && self.isSubscribed { + self.sendWantConfig() + } }) } diff --git a/Meshtastic/Persistence/UpdateCoreData.swift b/Meshtastic/Persistence/UpdateCoreData.swift index 8bc5b226..a203964f 100644 --- a/Meshtastic/Persistence/UpdateCoreData.swift +++ b/Meshtastic/Persistence/UpdateCoreData.swift @@ -8,20 +8,22 @@ import CoreData import MeshtasticProtobufs import OSLog -public func clearStaleNodes(nodeExpireDays: Int, context: NSManagedObjectContext) { +public func clearStaleNodes(nodeExpireDays: Int, context: NSManagedObjectContext) -> Bool { var nodeExpireTime: TimeInterval { return TimeInterval(-nodeExpireDays * 86400) } + var nodePKIExpireTime: TimeInterval { + return TimeInterval(-7 * 86400) + } if nodeExpireDays == 0 { // Purge Disabled Logger.data.info("💾 [NodeInfoEntity] Skip clearing stale nodes") - return + return false } let fetchRequest = NSFetchRequest(entityName: "NodeInfoEntity") - fetchRequest.predicate = NSPredicate(format: "lastHeard < %@ and favorite == false and ignored == false", - NSDate(timeIntervalSinceNow: nodeExpireTime)) - + fetchRequest.predicate = NSPredicate(format: "favorite == false AND ignored == false AND ((user.pkiEncrypted == NO AND lastHeard < %@) OR (user.pkiEncrypted == YES AND lastHeard < %@))", + NSDate(timeIntervalSinceNow: nodeExpireTime), NSDate(timeIntervalSinceNow: nodePKIExpireTime)) let batchDeleteRequest = NSBatchDeleteRequest(fetchRequest: fetchRequest) batchDeleteRequest.resultType = .resultTypeCount @@ -31,6 +33,9 @@ public func clearStaleNodes(nodeExpireDays: Int, context: NSManagedObjectContext try context.save() let deletedNodes = batchDeleteResult.result as? Int ?? 0 Logger.data.info("💾 [NodeInfoEntity] Cleared \(deletedNodes) stale nodes") + if deletedNodes > 0 { + return true + } } else { Logger.data.error("💥 [NodeInfoEntity] bad delete results") } @@ -38,6 +43,7 @@ public func clearStaleNodes(nodeExpireDays: Int, context: NSManagedObjectContext context.rollback() Logger.data.error("💥 [NodeInfoEntity] Error deleting stale nodes") } + return false } public func clearPax(destNum: Int64, context: NSManagedObjectContext) -> Bool { From 0f21ea9599bf8bc143253fbe2f115b29d5f178e4 Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Thu, 19 Jun 2025 16:06:02 -0700 Subject: [PATCH 153/213] Minimum expiry for PKI nodes --- Meshtastic/Persistence/UpdateCoreData.swift | 2 +- Meshtastic/Views/Settings/AppSettings.swift | 4 ++++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/Meshtastic/Persistence/UpdateCoreData.swift b/Meshtastic/Persistence/UpdateCoreData.swift index a203964f..6d085b21 100644 --- a/Meshtastic/Persistence/UpdateCoreData.swift +++ b/Meshtastic/Persistence/UpdateCoreData.swift @@ -13,7 +13,7 @@ public func clearStaleNodes(nodeExpireDays: Int, context: NSManagedObjectContext return TimeInterval(-nodeExpireDays * 86400) } var nodePKIExpireTime: TimeInterval { - return TimeInterval(-7 * 86400) + return TimeInterval((nodeExpireDays < 7 ? -7 : -nodeExpireDays) * 86400) } if nodeExpireDays == 0 { diff --git a/Meshtastic/Views/Settings/AppSettings.swift b/Meshtastic/Views/Settings/AppSettings.swift index c64e049c..6c6cd44d 100644 --- a/Meshtastic/Views/Settings/AppSettings.swift +++ b/Meshtastic/Views/Settings/AppSettings.swift @@ -6,6 +6,7 @@ import MapKit import OSLog struct AppSettings: View { + private var idiom: UIUserInterfaceIdiom { UIDevice.current.userInterfaceIdiom } @Environment(\.managedObjectContext) var context @EnvironmentObject var bleManager: BLEManager @State var totalDownloadedTileSize = "" @@ -71,6 +72,9 @@ struct AppSettings: View { Text("180") } } + Text("Nodes without PKI keys are cleared from the app database on the schedule set by the user, nodes with PKI keys are cleared only if the interval is set to 7 days or longer.") + .foregroundStyle(.secondary) + .font(idiom == .phone ? .caption : .callout) } Button { isPresentingCoreDataResetConfirm = true From ada2d0cfaaa3eb0f2fd0084adfb5678ea821929d Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Thu, 19 Jun 2025 16:07:47 -0700 Subject: [PATCH 154/213] Update purge node informational text --- 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 6c6cd44d..7bf044e6 100644 --- a/Meshtastic/Views/Settings/AppSettings.swift +++ b/Meshtastic/Views/Settings/AppSettings.swift @@ -72,7 +72,7 @@ struct AppSettings: View { Text("180") } } - Text("Nodes without PKI keys are cleared from the app database on the schedule set by the user, nodes with PKI keys are cleared only if the interval is set to 7 days or longer.") + Text("Nodes without PKI keys are cleared from the app database on the schedule set by the user, nodes with PKI keys are cleared only if the interval is set to 7 days or longer. This feature only purges nodes from the app that are not stored in the device node database.") .foregroundStyle(.secondary) .font(idiom == .phone ? .caption : .callout) } From 2c68b4e8d2a0823467effac61438e3468e94f1bf Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Thu, 19 Jun 2025 16:12:44 -0700 Subject: [PATCH 155/213] Update Meshtastic/Helpers/BLEManager.swift Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- Meshtastic/Helpers/BLEManager.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Meshtastic/Helpers/BLEManager.swift b/Meshtastic/Helpers/BLEManager.swift index e422a13f..51b01ba7 100644 --- a/Meshtastic/Helpers/BLEManager.swift +++ b/Meshtastic/Helpers/BLEManager.swift @@ -37,7 +37,7 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate var timeoutTimer: Timer? var timeoutTimerCount = 0 var positionTimer: Timer? - var maintenenceTimer: Timer? + var maintenanceTimer: Timer? let mqttManager = MqttClientProxyManager.shared var wantRangeTestPackets = false var wantStoreAndForwardPackets = false From 67a2b0631fcce0a1389d9254caf6e20264fafd02 Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Thu, 19 Jun 2025 16:14:09 -0700 Subject: [PATCH 156/213] Bump timer to every hour, fix spelling typo --- Localizable.xcstrings | 3 +++ Meshtastic/Helpers/BLEManager.swift | 4 ++-- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/Localizable.xcstrings b/Localizable.xcstrings index ee4749a8..99f6dd10 100644 --- a/Localizable.xcstrings +++ b/Localizable.xcstrings @@ -19830,6 +19830,9 @@ } } } + }, + "Nodes without PKI keys are cleared from the app database on the schedule set by the user, nodes with PKI 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." : { + }, "None" : { "localizations" : { diff --git a/Meshtastic/Helpers/BLEManager.swift b/Meshtastic/Helpers/BLEManager.swift index 51b01ba7..06033468 100644 --- a/Meshtastic/Helpers/BLEManager.swift +++ b/Meshtastic/Helpers/BLEManager.swift @@ -87,8 +87,8 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate super.init() centralManager = CBCentralManager(delegate: self, queue: nil) mqttManager.delegate = self - // Run clearStaleNodes every 10 minutes - maintenenceTimer = Timer.scheduledTimer(withTimeInterval: 60, repeats: true, block: { _ in + // Run clearStaleNodes every hour + maintenanceTimer = Timer.scheduledTimer(withTimeInterval: 3600, repeats: true, block: { _ in let result = clearStaleNodes(nodeExpireDays: Int(self.purgeStaleNodeDays), context: self.context) // If you are connected and the clear worked, pull nodes back from the node in case we have deleted anything from that app that is in the device nodedb if result && self.isSubscribed { From fff71755874a0b2a9ab3b660a2ad16288bfa61b0 Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Thu, 19 Jun 2025 17:47:02 -0700 Subject: [PATCH 157/213] Update purge nodes description --- 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 7bf044e6..93d1e8da 100644 --- a/Meshtastic/Views/Settings/AppSettings.swift +++ b/Meshtastic/Views/Settings/AppSettings.swift @@ -72,7 +72,7 @@ struct AppSettings: View { Text("180") } } - Text("Nodes without PKI keys are cleared from the app database on the schedule set by the user, nodes with PKI 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. Nodes without PKC keys are cleared from the app database on the schedule set by the user, nodes with PKC keys are cleared only if the interval is set to 7 days or longer. This feature only purges nodes from the app that are not stored in the device node database.") .foregroundStyle(.secondary) .font(idiom == .phone ? .caption : .callout) } From d7ad7a7e7278fc096e900d6dd1f93304bd64f933 Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Thu, 19 Jun 2025 23:17:40 -0700 Subject: [PATCH 158/213] Persistent Tip style for bluetooth, messages and administration --- Localizable.xcstrings | 6 ++-- Meshtastic.xcodeproj/project.pbxproj | 4 +++ Meshtastic/MeshtasticApp.swift | 7 ++--- Meshtastic/Tips/PersistantTips.swift | 37 ++++++++++++++++++++++++ Meshtastic/Views/Bluetooth/Connect.swift | 3 +- Meshtastic/Views/Messages/Messages.swift | 1 + Meshtastic/Views/Settings/Settings.swift | 1 + 7 files changed, 51 insertions(+), 8 deletions(-) create mode 100644 Meshtastic/Tips/PersistantTips.swift diff --git a/Localizable.xcstrings b/Localizable.xcstrings index 99f6dd10..e5553909 100644 --- a/Localizable.xcstrings +++ b/Localizable.xcstrings @@ -11809,6 +11809,9 @@ } } } + }, + "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." : { + }, "Favorites" : { "localizations" : { @@ -19830,9 +19833,6 @@ } } } - }, - "Nodes without PKI keys are cleared from the app database on the schedule set by the user, nodes with PKI 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." : { - }, "None" : { "localizations" : { diff --git a/Meshtastic.xcodeproj/project.pbxproj b/Meshtastic.xcodeproj/project.pbxproj index 883f3571..07cc1fb3 100644 --- a/Meshtastic.xcodeproj/project.pbxproj +++ b/Meshtastic.xcodeproj/project.pbxproj @@ -100,6 +100,7 @@ DD1BEF4A2E0292320090CE24 /* KeychainHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD1BEF492E0292220090CE24 /* KeychainHelper.swift */; }; DD1BEF4C2E030D310090CE24 /* KeyBackupStatus.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD1BEF4B2E030D240090CE24 /* KeyBackupStatus.swift */; }; DD1BEF4E2E03916A0090CE24 /* ChannelsHelp.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD1BEF4D2E0391620090CE24 /* ChannelsHelp.swift */; }; + DD1BEF502E0528AA0090CE24 /* PersistantTips.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD1BEF4F2E0528A80090CE24 /* PersistantTips.swift */; }; DD1BF2F92776FE2E008C8D2F /* UserMessageList.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD1BF2F82776FE2E008C8D2F /* UserMessageList.swift */; }; DD2160AF28C5552500C17253 /* MQTTConfig.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD2160AE28C5552500C17253 /* MQTTConfig.swift */; }; DD23A50F26FD1B4400D9B90C /* PeripheralModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD23A50E26FD1B4400D9B90C /* PeripheralModel.swift */; }; @@ -378,6 +379,7 @@ DD1BEF492E0292220090CE24 /* KeychainHelper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeychainHelper.swift; sourceTree = ""; }; DD1BEF4B2E030D240090CE24 /* KeyBackupStatus.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeyBackupStatus.swift; sourceTree = ""; }; DD1BEF4D2E0391620090CE24 /* ChannelsHelp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChannelsHelp.swift; sourceTree = ""; }; + DD1BEF4F2E0528A80090CE24 /* PersistantTips.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PersistantTips.swift; sourceTree = ""; }; DD1BF2F82776FE2E008C8D2F /* UserMessageList.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserMessageList.swift; sourceTree = ""; }; DD2160AE28C5552500C17253 /* MQTTConfig.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MQTTConfig.swift; sourceTree = ""; }; DD23A50E26FD1B4400D9B90C /* PeripheralModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PeripheralModel.swift; sourceTree = ""; }; @@ -863,6 +865,7 @@ DD7709392AA1ABA1007A8BF0 /* Tips */ = { isa = PBXGroup; children = ( + DD1BEF4F2E0528A80090CE24 /* PersistantTips.swift */, DD77093A2AA1ABB8007A8BF0 /* BluetoothTips.swift */, DD77093C2AA1AFA3007A8BF0 /* ChannelTips.swift */, DDC1B8192AB5377B00C71E39 /* MessagesTips.swift */, @@ -1439,6 +1442,7 @@ 25F5D5BE2C3F6D87008036E3 /* NavigationState.swift in Sources */, 2373AE152D0A24930086C749 /* MetricsSeriesList.swift in Sources */, DD354FD92BD96A0B0061A25F /* IAQScale.swift in Sources */, + DD1BEF502E0528AA0090CE24 /* PersistantTips.swift in Sources */, DDDB445429F8AD1600EE2349 /* Data.swift in Sources */, DDDB26462AACC0B7003AFCB7 /* NodeInfoItem.swift in Sources */, DDE5B4042B2279A700FCDD05 /* TraceRouteLog.swift in Sources */, diff --git a/Meshtastic/MeshtasticApp.swift b/Meshtastic/MeshtasticApp.swift index 9a0e9165..1512cae2 100644 --- a/Meshtastic/MeshtasticApp.swift +++ b/Meshtastic/MeshtasticApp.swift @@ -28,17 +28,16 @@ struct MeshtasticAppleApp: App { router: Router() ) self._appState = ObservedObject(wrappedValue: appState) - // Initialize the BLEManager singleton with the necessary dependencies BLEManager.setup(appState: appState, context: persistenceController.container.viewContext) self.persistenceController = persistenceController - // Wire up router self.appDelegate.router = appState.router - // Show Tips + #if DEBUG + // Show tips in development try? Tips.resetDatastore() + #endif } - var body: some Scene { WindowGroup { ContentView( diff --git a/Meshtastic/Tips/PersistantTips.swift b/Meshtastic/Tips/PersistantTips.swift new file mode 100644 index 00000000..24093285 --- /dev/null +++ b/Meshtastic/Tips/PersistantTips.swift @@ -0,0 +1,37 @@ +// +// Untitled.swift +// Meshtastic +// +// Created by Garth Vander Houwen on 6/19/25. +// +import TipKit + +struct PersistentTip: TipViewStyle { + func makeBody(configuration: Configuration) -> some View { + VStack { + HStack(alignment: .top) { + if let image = configuration.image { + image + .font(.system(size: 42)) + .foregroundColor(.accentColor) + .padding(.trailing, 5) + } + VStack(alignment: .leading) { + if let title = configuration.title { + title + .bold() + .font(.headline) + } + if let message = configuration.message { + message + .foregroundStyle(.secondary) + .font(.callout) + } + } + } + } + .frame(maxWidth: .infinity) + .backgroundStyle(.thinMaterial) + .padding(.top, 5) + } +} diff --git a/Meshtastic/Views/Bluetooth/Connect.swift b/Meshtastic/Views/Bluetooth/Connect.swift index 1176241a..37ca7966 100644 --- a/Meshtastic/Views/Bluetooth/Connect.swift +++ b/Meshtastic/Views/Bluetooth/Connect.swift @@ -46,9 +46,10 @@ struct Connect: View { VStack { List { if bleManager.isSwitchedOn { - Section(header: Text("Connected Radio").font(.title)) { + Section { if let connectedPeripheral = bleManager.connectedPeripheral, connectedPeripheral.peripheral.state == .connected { TipView(BluetoothConnectionTip(), arrowEdge: .bottom) + .tipViewStyle(PersistentTip()) VStack(alignment: .leading) { HStack { VStack(alignment: .center) { diff --git a/Meshtastic/Views/Messages/Messages.swift b/Meshtastic/Views/Messages/Messages.swift index cb6947c0..8a75faf7 100644 --- a/Meshtastic/Views/Messages/Messages.swift +++ b/Meshtastic/Views/Messages/Messages.swift @@ -64,6 +64,7 @@ struct Messages: View { } TipView(MessagesTip(), arrowEdge: .top) + .tipViewStyle(PersistentTip()) } .navigationTitle("Messages") .navigationBarTitleDisplayMode(.large) diff --git a/Meshtastic/Views/Settings/Settings.swift b/Meshtastic/Views/Settings/Settings.swift index a4b664b9..426d95e9 100644 --- a/Meshtastic/Views/Settings/Settings.swift +++ b/Meshtastic/Views/Settings/Settings.swift @@ -440,6 +440,7 @@ struct Settings: View { } } TipView(AdminChannelTip(), arrowEdge: .top) + .tipViewStyle(PersistentTip()) } else { if bleManager.connectedPeripheral != nil { Text("Connected Node \(node?.user?.longName?.addingVariationSelectors ?? "Unknown".localized)") From b1aa91778451e82d59386b5377e44e3d60f0a138 Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Thu, 19 Jun 2025 23:49:23 -0700 Subject: [PATCH 159/213] Clean up context menu on connnected radio --- Localizable.xcstrings | 7 +++++++ Meshtastic/Views/Bluetooth/Connect.swift | 8 +++----- 2 files changed, 10 insertions(+), 5 deletions(-) diff --git a/Localizable.xcstrings b/Localizable.xcstrings index e5553909..bd3e50bc 100644 --- a/Localizable.xcstrings +++ b/Localizable.xcstrings @@ -4365,8 +4365,12 @@ } } } + }, + "BLE RSSI %lld" : { + }, "BLE RSSI: %lld" : { + "extractionState" : "stale", "localizations" : { "it" : { "stringUnit" : { @@ -16251,6 +16255,7 @@ } }, "Long Name: %@" : { + "extractionState" : "stale", "localizations" : { "de" : { "stringUnit" : { @@ -20047,6 +20052,7 @@ } }, "Num: %@" : { + "extractionState" : "stale", "localizations" : { "de" : { "stringUnit" : { @@ -28155,6 +28161,7 @@ } }, "Short Name: %@" : { + "extractionState" : "stale", "localizations" : { "de" : { "stringUnit" : { diff --git a/Meshtastic/Views/Bluetooth/Connect.swift b/Meshtastic/Views/Bluetooth/Connect.swift index 37ca7966..53e27ae6 100644 --- a/Meshtastic/Views/Bluetooth/Connect.swift +++ b/Meshtastic/Views/Bluetooth/Connect.swift @@ -105,6 +105,8 @@ struct Connect: View { .contextMenu { if node != nil { + Label("\(String(node!.num))", systemImage: "number") + Label("BLE RSSI \(connectedPeripheral.rssi)", systemImage: "cellularbars") #if !targetEnvironment(macCatalyst) if bleManager.isSubscribed { Button { @@ -124,10 +126,6 @@ struct Connect: View { } } #endif - Text("Num: \(String(node!.num))") - Text("Short Name: \(node?.user?.shortName ?? "?")") - Text("Long Name: \(node?.user?.longName?.addingVariationSelectors ?? "Unknown".localized)") - Text("BLE RSSI: \(connectedPeripheral.rssi)") if bleManager.allowDisconnect { Button(role: .destructive) { if let connectedPeripheral = bleManager.connectedPeripheral, @@ -137,7 +135,7 @@ struct Connect: View { } label: { Label("Disconnect", systemImage: "antenna.radiowaves.left.and.right.slash") } - Button { + Button(role: .destructive) { if !bleManager.sendShutdown(fromUser: node!.user!, toUser: node!.user!) { Logger.mesh.error("Shutdown Failed") } From 942b72dd99667d167dfd266e0a2dc01e00c11d99 Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Thu, 19 Jun 2025 23:51:52 -0700 Subject: [PATCH 160/213] Remove stale translations --- Localizable.xcstrings | 192 ------------------------------------------ 1 file changed, 192 deletions(-) diff --git a/Localizable.xcstrings b/Localizable.xcstrings index bd3e50bc..37f31711 100644 --- a/Localizable.xcstrings +++ b/Localizable.xcstrings @@ -2268,41 +2268,6 @@ } } }, - "Admin & Direct Message Keys" : { - "extractionState" : "stale", - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Schlüssel für Administrator und Direktnachrichten" - } - }, - "it" : { - "stringUnit" : { - "state" : "translated", - "value" : "Tasti amministratore e messaggi diretti" - } - }, - "sr" : { - "stringUnit" : { - "state" : "translated", - "value" : "Админ и кључеви директних порука" - } - }, - "zh-Hans" : { - "stringUnit" : { - "state" : "translated", - "value" : "管理员 & 私信密钥" - } - }, - "zh-Hant-TW" : { - "stringUnit" : { - "state" : "translated", - "value" : "管理與直接訊息加密金鑰" - } - } - } - }, "Admin Keys" : { }, @@ -4368,35 +4333,6 @@ }, "BLE RSSI %lld" : { - }, - "BLE RSSI: %lld" : { - "extractionState" : "stale", - "localizations" : { - "it" : { - "stringUnit" : { - "state" : "translated", - "value" : "RSSI BLE: %lld" - } - }, - "sr" : { - "stringUnit" : { - "state" : "translated", - "value" : "BLE RSSI: %lld" - } - }, - "zh-Hans" : { - "stringUnit" : { - "state" : "translated", - "value" : "BLE RSSI: %lld" - } - }, - "zh-Hant-TW" : { - "stringUnit" : { - "state" : "translated", - "value" : "藍牙訊號強度(RSSI):%lld" - } - } - } }, "BLE: %@" : { "localizations" : { @@ -16254,41 +16190,6 @@ } } }, - "Long Name: %@" : { - "extractionState" : "stale", - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Langer Name: %@" - } - }, - "it" : { - "stringUnit" : { - "state" : "translated", - "value" : "Nome lungo: %@" - } - }, - "sr" : { - "stringUnit" : { - "state" : "translated", - "value" : "Дуго име: %@" - } - }, - "zh-Hans" : { - "stringUnit" : { - "state" : "translated", - "value" : "长名称: %@" - } - }, - "zh-Hant-TW" : { - "stringUnit" : { - "state" : "translated", - "value" : "完整名稱:%@" - } - } - } - }, "Long press to favorite or mute the contact or delete a conversation." : { "localizations" : { "it" : { @@ -20051,35 +19952,6 @@ } } }, - "Num: %@" : { - "extractionState" : "stale", - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Anzahl: %@" - } - }, - "it" : { - "stringUnit" : { - "state" : "translated", - "value" : "Num: %@" - } - }, - "sr" : { - "stringUnit" : { - "state" : "translated", - "value" : "Број: %@" - } - }, - "zh-Hant-TW" : { - "stringUnit" : { - "state" : "translated", - "value" : "Num: %@" - } - } - } - }, "Number of hops" : { "localizations" : { "de" : { @@ -27295,35 +27167,6 @@ } } }, - "Sent out to other nodes on the mesh to allow them to compute a shared secret key." : { - "extractionState" : "stale", - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Wird an andere Knoten im Netz gesendet, damit diese einen gemeinsamen geheimen Schlüssel berechnen können." - } - }, - "it" : { - "stringUnit" : { - "state" : "translated", - "value" : "Inviato agli altri nodi della rete per consentire loro di calcolare una chiave segreta condivisa." - } - }, - "sr" : { - "stringUnit" : { - "state" : "translated", - "value" : "Послато другим чворовима на меш мрежи како би им омогућило да израчунају заједнички тајни кључ." - } - }, - "zh-Hant-TW" : { - "stringUnit" : { - "state" : "translated", - "value" : "傳送到網路上的其他節點,以便共同計算一組共享私鑰。" - } - } - } - }, "Sequence number" : { "localizations" : { "de" : { @@ -28160,41 +28003,6 @@ } } }, - "Short Name: %@" : { - "extractionState" : "stale", - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Kurzname: %@" - } - }, - "it" : { - "stringUnit" : { - "state" : "translated", - "value" : "Nome breve: %@" - } - }, - "sr" : { - "stringUnit" : { - "state" : "translated", - "value" : "Кратко име: %@" - } - }, - "zh-Hans" : { - "stringUnit" : { - "state" : "translated", - "value" : "短名称: %@" - } - }, - "zh-Hant-TW" : { - "stringUnit" : { - "state" : "translated", - "value" : "簡短名稱:%@" - } - } - } - }, "Short Range - Fast" : { "localizations" : { "it" : { From eb725298eaa2be66994cb707cbfbbb036da9eea6 Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Fri, 20 Jun 2025 06:27:47 -0700 Subject: [PATCH 161/213] Bump version down, trap additional key error --- Meshtastic.xcodeproj/project.pbxproj | 8 ++++---- Meshtastic/Helpers/BLEManager.swift | 3 ++- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/Meshtastic.xcodeproj/project.pbxproj b/Meshtastic.xcodeproj/project.pbxproj index 07cc1fb3..956f7e9f 100644 --- a/Meshtastic.xcodeproj/project.pbxproj +++ b/Meshtastic.xcodeproj/project.pbxproj @@ -1824,7 +1824,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 2.6.9; + MARKETING_VERSION = 2.6.8; PRODUCT_BUNDLE_IDENTIFIER = gvh.MeshtasticClient; PRODUCT_NAME = "$(TARGET_NAME)"; SUPPORTS_MACCATALYST = YES; @@ -1857,7 +1857,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 2.6.9; + MARKETING_VERSION = 2.6.8; PRODUCT_BUNDLE_IDENTIFIER = gvh.MeshtasticClient; PRODUCT_NAME = "$(TARGET_NAME)"; SUPPORTS_MACCATALYST = YES; @@ -1888,7 +1888,7 @@ "@executable_path/Frameworks", "@executable_path/../../Frameworks", ); - MARKETING_VERSION = 2.6.9; + MARKETING_VERSION = 2.6.8; PRODUCT_BUNDLE_IDENTIFIER = gvh.MeshtasticClient.Widgets; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; @@ -1920,7 +1920,7 @@ "@executable_path/Frameworks", "@executable_path/../../Frameworks", ); - MARKETING_VERSION = 2.6.9; + MARKETING_VERSION = 2.6.8; PRODUCT_BUNDLE_IDENTIFIER = gvh.MeshtasticClient.Widgets; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; diff --git a/Meshtastic/Helpers/BLEManager.swift b/Meshtastic/Helpers/BLEManager.swift index 06033468..ffe00410 100644 --- a/Meshtastic/Helpers/BLEManager.swift +++ b/Meshtastic/Helpers/BLEManager.swift @@ -740,7 +740,8 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate let nsError = error as NSError Logger.data.error("💥 [TraceRouteEntity] Error Updating Core Data: \(nsError, privacy: .public)") } - } else if decodedInfo.clientNotification.message.starts(with: "You Device is configured with a low entropy") || decodedInfo.clientNotification.message.starts(with: "Compromised keys detected") { + } else if decodedInfo.clientNotification.message.starts(with: "You Device is configured with a low entropy") || decodedInfo.clientNotification.message.starts(with: "Compromised keys detected") + || decodedInfo.clientNotification.message.starts(with: "Remote device"){ path = "meshtastic:///settings/security" } } From 6a958f62bbc9ad2eb7fff3e49c305a77c7dc9d23 Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Fri, 20 Jun 2025 06:41:07 -0700 Subject: [PATCH 162/213] Add padding back --- Meshtastic/Tips/PersistantTips.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Meshtastic/Tips/PersistantTips.swift b/Meshtastic/Tips/PersistantTips.swift index 24093285..4cf9858b 100644 --- a/Meshtastic/Tips/PersistantTips.swift +++ b/Meshtastic/Tips/PersistantTips.swift @@ -32,6 +32,6 @@ struct PersistentTip: TipViewStyle { } .frame(maxWidth: .infinity) .backgroundStyle(.thinMaterial) - .padding(.top, 5) + .padding() } } From 32cb7fdeface055f64b9e9d35f6a732862341a10 Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Fri, 20 Jun 2025 06:48:58 -0700 Subject: [PATCH 163/213] Key regeneration description update --- Meshtastic/Views/Settings/Config/SecurityConfig.swift | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Meshtastic/Views/Settings/Config/SecurityConfig.swift b/Meshtastic/Views/Settings/Config/SecurityConfig.swift index d66fe414..8a927993 100644 --- a/Meshtastic/Views/Settings/Config/SecurityConfig.swift +++ b/Meshtastic/Views/Settings/Config/SecurityConfig.swift @@ -162,7 +162,8 @@ struct SecurityConfig: View { .buttonBorderShape(.capsule) .controlSize(.small) } - Text("Generate a new private key to replace the one currently in use. Public key will automatically be regenerated as well.") + Text("Generate a new private key to replace the one currently in use. The public key will automatically be regenerated from your private key.") + .font(idiom == .phone ? .caption : .callout) } } Section(header: Text("Admin Keys")) { From 5868261f6dd0ffb85ee310b7f3ebbe2de070e3ee Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Fri, 20 Jun 2025 06:50:07 -0700 Subject: [PATCH 164/213] Key text translation update --- Localizable.xcstrings | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Localizable.xcstrings b/Localizable.xcstrings index 37f31711..28dd7f9c 100644 --- a/Localizable.xcstrings +++ b/Localizable.xcstrings @@ -13073,7 +13073,7 @@ } } }, - "Generate a new private key to replace the one currently in use. Public key will automatically be regenerated as well." : { + "Generate a new private key to replace the one currently in use. The public key will automatically be regenerated from your private key." : { }, "Generate QR Code" : { From 2a96457a424adb20801bff7a86bc04139cf8ce08 Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Fri, 20 Jun 2025 06:52:57 -0700 Subject: [PATCH 165/213] adjust forground style of key regenerate description --- Meshtastic/Views/Settings/Config/SecurityConfig.swift | 1 + 1 file changed, 1 insertion(+) diff --git a/Meshtastic/Views/Settings/Config/SecurityConfig.swift b/Meshtastic/Views/Settings/Config/SecurityConfig.swift index 8a927993..e252819a 100644 --- a/Meshtastic/Views/Settings/Config/SecurityConfig.swift +++ b/Meshtastic/Views/Settings/Config/SecurityConfig.swift @@ -163,6 +163,7 @@ struct SecurityConfig: View { .controlSize(.small) } Text("Generate a new private key to replace the one currently in use. The public key will automatically be regenerated from your private key.") + .foregroundStyle(.secondary) .font(idiom == .phone ? .caption : .callout) } } From 365699d1d41f12b1fdab62c47a0f0523a621bfa0 Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Fri, 20 Jun 2025 07:12:54 -0700 Subject: [PATCH 166/213] fix typo --- Meshtastic/Tips/BluetoothTips.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Meshtastic/Tips/BluetoothTips.swift b/Meshtastic/Tips/BluetoothTips.swift index 838d29fc..72c22108 100644 --- a/Meshtastic/Tips/BluetoothTips.swift +++ b/Meshtastic/Tips/BluetoothTips.swift @@ -16,7 +16,7 @@ struct BluetoothConnectionTip: Tip { Text("Connected Radio") } var message: Text? { - Text("Shows information for the Lora radio connected via bluetooth. You can swipe left to disconnect the radio and long press start the live activity.") + Text("Shows information for the Lora radio connected via bluetooth. You can swipe left to disconnect the radio and long press to start the live activity.") } var image: Image? { Image(systemName: "flipphone") From 57ac6be7454b848dd5af5f90c41cbf88206eb2b9 Mon Sep 17 00:00:00 2001 From: "David J. kordsmeier" Date: Sat, 21 Jun 2025 11:10:38 +0900 Subject: [PATCH 167/213] Add initial Japanese localization file --- Localizable.xcstrings | 6 ++++++ Meshtastic.xcodeproj/project.pbxproj | 1 + 2 files changed, 7 insertions(+) diff --git a/Localizable.xcstrings b/Localizable.xcstrings index 28dd7f9c..ea67b2a9 100644 --- a/Localizable.xcstrings +++ b/Localizable.xcstrings @@ -312,6 +312,12 @@ "value" : "%@ - Nessuna risposta" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "%@ - 応答なし" + } + }, "sr" : { "stringUnit" : { "state" : "translated", diff --git a/Meshtastic.xcodeproj/project.pbxproj b/Meshtastic.xcodeproj/project.pbxproj index 956f7e9f..df097b06 100644 --- a/Meshtastic.xcodeproj/project.pbxproj +++ b/Meshtastic.xcodeproj/project.pbxproj @@ -1296,6 +1296,7 @@ se, sr, it, + ja, ); mainGroup = DDC2E14B26CE248E0042C5E4; packageReferences = ( From bd88e1fdd325f81b8d0a9af6b53fdd54b4af3532 Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Sun, 22 Jun 2025 20:19:42 -0700 Subject: [PATCH 168/213] Bump version, add channel help to settings channel update locks to be red only if precise location is being shared on an unencrypted channel --- Localizable.xcstrings | 14 ++++++- Meshtastic.xcodeproj/project.pbxproj | 4 ++ Meshtastic/Helpers/BLEManager.swift | 6 +-- Meshtastic/Views/Helpers/ChannelLock.swift | 35 ++++++++++++++++ .../Views/Helpers/Help/ChannelsHelp.swift | 33 ++++++++++++--- Meshtastic/Views/Messages/ChannelList.swift | 10 +---- Meshtastic/Views/Settings/Channels.swift | 33 +++++++++++---- Meshtastic/Views/Settings/ShareChannels.swift | 40 ++++++++++++++----- 8 files changed, 138 insertions(+), 37 deletions(-) create mode 100644 Meshtastic/Views/Helpers/ChannelLock.swift diff --git a/Localizable.xcstrings b/Localizable.xcstrings index 28dd7f9c..ba0d7976 100644 --- a/Localizable.xcstrings +++ b/Localizable.xcstrings @@ -1693,7 +1693,7 @@ } } }, - "A channel index of 0 indicates the primary channel where all broadcast packets are sent from." : { + "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." : { }, "A green lock means the channel is securely encrypted with either a 128 or 256 bit AES key." : { @@ -1757,7 +1757,10 @@ } } }, - "A red lock with a slash means the channel is not securely encrypted, it uses either no key at all or a 1 byte known key. Traffic on this channel is easily intercepted." : { + "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." : { + + }, + "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." : { }, "A Trace Route was sent, no response has been received." : { @@ -1781,6 +1784,9 @@ } } } + }, + "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." : { + }, "About" : { "localizations" : { @@ -28238,6 +28244,7 @@ } }, "Shows information for the Lora radio connected via bluetooth. You can swipe left to disconnect the radio and long press start the live activity." : { + "extractionState" : "stale", "localizations" : { "it" : { "stringUnit" : { @@ -28252,6 +28259,9 @@ } } } + }, + "Shows information for the Lora radio connected via bluetooth. You can swipe left to disconnect the radio and long press to start the live activity." : { + }, "Shut Down" : { "localizations" : { diff --git a/Meshtastic.xcodeproj/project.pbxproj b/Meshtastic.xcodeproj/project.pbxproj index 956f7e9f..876c41c5 100644 --- a/Meshtastic.xcodeproj/project.pbxproj +++ b/Meshtastic.xcodeproj/project.pbxproj @@ -101,6 +101,7 @@ DD1BEF4C2E030D310090CE24 /* KeyBackupStatus.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD1BEF4B2E030D240090CE24 /* KeyBackupStatus.swift */; }; DD1BEF4E2E03916A0090CE24 /* ChannelsHelp.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD1BEF4D2E0391620090CE24 /* ChannelsHelp.swift */; }; DD1BEF502E0528AA0090CE24 /* PersistantTips.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD1BEF4F2E0528A80090CE24 /* PersistantTips.swift */; }; + DD1BEF522E08E9B80090CE24 /* ChannelLock.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD1BEF512E08E9AE0090CE24 /* ChannelLock.swift */; }; DD1BF2F92776FE2E008C8D2F /* UserMessageList.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD1BF2F82776FE2E008C8D2F /* UserMessageList.swift */; }; DD2160AF28C5552500C17253 /* MQTTConfig.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD2160AE28C5552500C17253 /* MQTTConfig.swift */; }; DD23A50F26FD1B4400D9B90C /* PeripheralModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD23A50E26FD1B4400D9B90C /* PeripheralModel.swift */; }; @@ -380,6 +381,7 @@ DD1BEF4B2E030D240090CE24 /* KeyBackupStatus.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeyBackupStatus.swift; sourceTree = ""; }; DD1BEF4D2E0391620090CE24 /* ChannelsHelp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChannelsHelp.swift; sourceTree = ""; }; DD1BEF4F2E0528A80090CE24 /* PersistantTips.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PersistantTips.swift; sourceTree = ""; }; + DD1BEF512E08E9AE0090CE24 /* ChannelLock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChannelLock.swift; sourceTree = ""; }; DD1BF2F82776FE2E008C8D2F /* UserMessageList.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserMessageList.swift; sourceTree = ""; }; DD2160AE28C5552500C17253 /* MQTTConfig.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MQTTConfig.swift; sourceTree = ""; }; DD23A50E26FD1B4400D9B90C /* PeripheralModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PeripheralModel.swift; sourceTree = ""; }; @@ -1057,6 +1059,7 @@ DD3CC6BD28E4CD9800FA9159 /* BatteryGauge.swift */, DD3CC24B2C498D6C001BD3A2 /* BatteryCompact.swift */, DD457187293C7E63000C49FB /* BLESignalStrengthIndicator.swift */, + DD1BEF512E08E9AE0090CE24 /* ChannelLock.swift */, DD47E3D526F17ED900029299 /* CircleText.swift */, DDF924C926FBB953009FE055 /* ConnectedDevice.swift */, DDD94A4F2845C8F5004A87A0 /* DateTimeText.swift */, @@ -1384,6 +1387,7 @@ 25F26B1E2C2F610D00C9CD9D /* Logger.swift in Sources */, 259792252C2F114500AD1659 /* ChannelEntityExtension.swift in Sources */, BCE2D3C52C7AE369008E6199 /* RestartNodeIntent.swift in Sources */, + DD1BEF522E08E9B80090CE24 /* ChannelLock.swift in Sources */, 259792262C2F114500AD1659 /* PositionEntityExtension.swift in Sources */, 259792272C2F114500AD1659 /* TraceRouteEntityExtension.swift in Sources */, DDDB444829F8A9C900EE2349 /* String.swift in Sources */, diff --git a/Meshtastic/Helpers/BLEManager.swift b/Meshtastic/Helpers/BLEManager.swift index ffe00410..389e812a 100644 --- a/Meshtastic/Helpers/BLEManager.swift +++ b/Meshtastic/Helpers/BLEManager.swift @@ -725,7 +725,6 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate let message = CocoaMQTTMessage(topic: decodedInfo.mqttClientProxyMessage.topic, payload: [UInt8](decodedInfo.mqttClientProxyMessage.data), retained: decodedInfo.mqttClientProxyMessage.retained) mqttManager.mqttClientProxy?.publish(message) } else if decodedInfo.payloadVariant == FromRadio.OneOf_PayloadVariant.clientNotification(decodedInfo.clientNotification) { - var path = "meshtastic:///settings/debugLogs" if decodedInfo.clientNotification.hasReplyID { /// Set Sent bool on TraceRouteEntity to false if we got rate limited @@ -740,8 +739,9 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate let nsError = error as NSError Logger.data.error("💥 [TraceRouteEntity] Error Updating Core Data: \(nsError, privacy: .public)") } - } else if decodedInfo.clientNotification.message.starts(with: "You Device is configured with a low entropy") || decodedInfo.clientNotification.message.starts(with: "Compromised keys detected") - || decodedInfo.clientNotification.message.starts(with: "Remote device"){ + } + if decodedInfo.clientNotification.payloadVariant == ClientNotification.OneOf_PayloadVariant.lowEntropyKey(decodedInfo.clientNotification.lowEntropyKey) || + decodedInfo.clientNotification.payloadVariant == ClientNotification.OneOf_PayloadVariant.duplicatedPublicKey(decodedInfo.clientNotification.duplicatedPublicKey) { path = "meshtastic:///settings/security" } } diff --git a/Meshtastic/Views/Helpers/ChannelLock.swift b/Meshtastic/Views/Helpers/ChannelLock.swift new file mode 100644 index 00000000..3a66dc5a --- /dev/null +++ b/Meshtastic/Views/Helpers/ChannelLock.swift @@ -0,0 +1,35 @@ +// +// ChannelLock.swift +// Meshtastic +// +// Copyright(c) Garth Vander Houwen 6/22/25. +// +import SwiftUI + +struct ChannelLock: View { + + @ObservedObject var channel: ChannelEntity + + var body: some View { + /// Unencrypted - using no key at all or a known 1 byte key + if channel.psk?.hexDescription.count ?? 0 < 3 { + let preciseLoction = 17...32 + // Using precise location and have MQTT uplink enabled + if channel.uplinkEnabled && preciseLoction ~= (Int(channel.positionPrecision)) { + Image(systemName: "lock.open.trianglebadge.exclamationmark.fill") + .foregroundColor(.red) + // Using precise location + } else if preciseLoction ~= (Int(channel.positionPrecision)) { + Image(systemName: "lock.open.fill") + .foregroundColor(.red) + // Just unencrypted without any location or MQTT + } else { + Image(systemName: "lock.open.fill") + .foregroundColor(.yellow) + } + } else { + Image(systemName: "lock.fill") + .foregroundColor(.green) + } + } +} diff --git a/Meshtastic/Views/Helpers/Help/ChannelsHelp.swift b/Meshtastic/Views/Helpers/Help/ChannelsHelp.swift index 830fe3cd..ad8b3b06 100644 --- a/Meshtastic/Views/Helpers/Help/ChannelsHelp.swift +++ b/Meshtastic/Views/Helpers/Help/ChannelsHelp.swift @@ -21,25 +21,46 @@ struct ChannelsHelp: View { CircleText(text: String(0), color: .accentColor) .brightness(0.2) .offset(y: -10) - Text("A channel index of 0 indicates the primary channel where all broadcast packets are sent from.") + Text("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.") .fixedSize(horizontal: false, vertical: true) .padding(.bottom) + .padding(.leading, 7) } HStack { Image(systemName: "lock.fill") - .padding(.bottom) + .padding(.leading) + .padding(.trailing, 7) .foregroundColor(Color.green) - .font(.largeTitle) + .font(.title) Text("A green lock means the channel is securely encrypted with either a 128 or 256 bit AES key.") .fixedSize(horizontal: false, vertical: true) .padding(.bottom) } HStack { - Image(systemName: "lock.slash.fill") + Image(systemName: "lock.open.fill") + .padding(.leading) + .foregroundColor(Color.yellow) + .font(.title) + Text("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.") + .fixedSize(horizontal: false, vertical: true) .padding(.bottom) + } + HStack { + Image(systemName: "lock.open.fill") + .padding(.leading) .foregroundColor(Color.red) - .font(.largeTitle) - Text("A red lock with a slash means the channel is not securely encrypted, it uses either no key at all or a 1 byte known key. Traffic on this channel is easily intercepted.") + .font(.title) + Text("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.") + .fixedSize(horizontal: false, vertical: true) + .padding(.bottom) + } + HStack { + Image(systemName: "lock.open.trianglebadge.exclamationmark.fill") + .padding(.leading) + .symbolRenderingMode(.multicolor) + .foregroundColor(Color.red) + .font(.title) + Text("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.") .fixedSize(horizontal: false, vertical: true) .padding(.bottom) } diff --git a/Meshtastic/Views/Messages/ChannelList.swift b/Meshtastic/Views/Messages/ChannelList.swift index 940396b9..835c662d 100644 --- a/Meshtastic/Views/Messages/ChannelList.swift +++ b/Meshtastic/Views/Messages/ChannelList.swift @@ -58,13 +58,7 @@ struct ChannelList: View { VStack(alignment: .leading) { HStack { - if channel.psk?.hexDescription.count ?? 0 < 3 { - Image(systemName: "lock.slash.fill") - .foregroundColor(.red) - } else { - Image(systemName: "lock.fill") - .foregroundColor(.green) - } + ChannelLock(channel: channel) if channel.name?.isEmpty ?? false { if channel.role == 1 { Text(String("PrimaryChannel").camelCaseToWords()) @@ -173,7 +167,7 @@ struct ChannelList: View { } .sheet(isPresented: $showingHelp) { ChannelsHelp() - .presentationDetents([.medium, .large]) + .presentationDetents([.large]) .presentationDragIndicator(.visible) } .safeAreaInset(edge: .bottom, alignment: .leading) { diff --git a/Meshtastic/Views/Settings/Channels.swift b/Meshtastic/Views/Settings/Channels.swift index d479403f..8e38f27b 100644 --- a/Meshtastic/Views/Settings/Channels.swift +++ b/Meshtastic/Views/Settings/Channels.swift @@ -47,6 +47,7 @@ struct Channels: View { /// Minimum Version for granular position configuration @State var minimumVersion = "2.2.24" + @State private var showingHelp = false @FetchRequest( sortDescriptors: [NSSortDescriptor(key: "favorite", ascending: false), @@ -124,13 +125,7 @@ struct Channels: View { .brightness(0.1) VStack { HStack { - if channel.psk?.hexDescription.count ?? 0 < 3 { - Image(systemName: "lock.slash.fill") - .foregroundColor(.red) - } else { - Image(systemName: "lock.fill") - .foregroundColor(.green) - } + ChannelLock(channel: channel) if channel.name?.isEmpty ?? false { if channel.role == 1 { Text(String("PrimaryChannel").camelCaseToWords()).font(.headline) @@ -246,6 +241,7 @@ struct Channels: View { #endif } } + if node?.myInfo?.channels?.array.count ?? 0 < 8 && node != nil { Button { @@ -286,6 +282,29 @@ struct Channels: View { .padding() } } + .sheet(isPresented: $showingHelp) { + ChannelsHelp() + .presentationDetents([.large]) + .presentationDragIndicator(.visible) + } + .safeAreaInset(edge: .bottom, alignment: .leading) { + HStack { + Button(action: { + withAnimation { + showingHelp = !showingHelp + } + }) { + Image(systemName: !showingHelp ? "questionmark.circle" : "questionmark.circle.fill") + .padding(.vertical, 5) + } + .tint(Color(UIColor.secondarySystemBackground)) + .foregroundColor(.accentColor) + .buttonStyle(.borderedProminent) + } + .controlSize(.regular) + .padding(5) + } + .padding(.bottom, 5) .navigationTitle("Channels") .navigationBarItems(trailing: ZStack { diff --git a/Meshtastic/Views/Settings/ShareChannels.swift b/Meshtastic/Views/Settings/ShareChannels.swift index 7e4068e2..a3788ff0 100644 --- a/Meshtastic/Views/Settings/ShareChannels.swift +++ b/Meshtastic/Views/Settings/ShareChannels.swift @@ -49,6 +49,7 @@ struct ShareChannels: View { var node: NodeInfoEntity? @State private var channelsUrl = "https://www.meshtastic.org/e/#" var qrCodeImage = QrCodeImage() + @State private var showingHelp = false var body: some View { @@ -82,13 +83,7 @@ struct ShareChannels: View { .toggleStyle(.switch) .labelsHidden() Text(((channel.name!.isEmpty ? "Primary" : channel.name) ?? "Primary").camelCaseToWords()) - if channel.psk?.hexDescription.count ?? 0 < 3 { - Image(systemName: "lock.slash.fill") - .foregroundColor(.red) - } else { - Image(systemName: "lock.fill") - .foregroundColor(.green) - } + ChannelLock(channel: channel) } else if channel.index == 1 && channel.role > 0 { Toggle("Channel 1 Included", isOn: $includeChannel1) .toggleStyle(.switch) @@ -216,16 +211,39 @@ struct ShareChannels: View { .resizable() .scaledToFit() .frame( - minWidth: smallest * (UIDevice.current.userInterfaceIdiom == .phone ? 0.8 : 0.6), - maxWidth: smallest * (UIDevice.current.userInterfaceIdiom == .phone ? 0.8 : 0.6), - minHeight: smallest * (UIDevice.current.userInterfaceIdiom == .phone ? 0.8 : 0.6), - maxHeight: smallest * (UIDevice.current.userInterfaceIdiom == .phone ? 0.8 : 0.6), + minWidth: smallest * (UIDevice.current.userInterfaceIdiom == .phone ? 0.75 : 0.6), + maxWidth: smallest * (UIDevice.current.userInterfaceIdiom == .phone ? 0.75 : 0.6), + minHeight: smallest * (UIDevice.current.userInterfaceIdiom == .phone ? 0.75 : 0.6), + maxHeight: smallest * (UIDevice.current.userInterfaceIdiom == .phone ? 0.75 : 0.6), alignment: .top ) } } } } + .sheet(isPresented: $showingHelp) { + ChannelsHelp() + .presentationDetents([.large]) + .presentationDragIndicator(.visible) + } + .safeAreaInset(edge: .bottom, alignment: .leading) { + HStack { + Button(action: { + withAnimation { + showingHelp = !showingHelp + } + }) { + Image(systemName: !showingHelp ? "questionmark.circle" : "questionmark.circle.fill") + .padding(.vertical, 5) + } + .tint(Color(UIColor.secondarySystemBackground)) + .foregroundColor(.accentColor) + .buttonStyle(.borderedProminent) + } + .controlSize(.regular) + .padding(5) + } + .padding(.bottom, 5) .navigationTitle("Generate QR Code") .navigationBarTitleDisplayMode(.inline) .navigationBarItems(trailing: From bd6f4ad7ca23ba184ad3ae62ab0442f336df1694 Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Sun, 22 Jun 2025 20:34:13 -0700 Subject: [PATCH 169/213] 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 876c41c5..55581bc9 100644 --- a/Meshtastic.xcodeproj/project.pbxproj +++ b/Meshtastic.xcodeproj/project.pbxproj @@ -1828,7 +1828,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 2.6.8; + MARKETING_VERSION = 2.6.9; PRODUCT_BUNDLE_IDENTIFIER = gvh.MeshtasticClient; PRODUCT_NAME = "$(TARGET_NAME)"; SUPPORTS_MACCATALYST = YES; @@ -1861,7 +1861,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 2.6.8; + MARKETING_VERSION = 2.6.9; PRODUCT_BUNDLE_IDENTIFIER = gvh.MeshtasticClient; PRODUCT_NAME = "$(TARGET_NAME)"; SUPPORTS_MACCATALYST = YES; @@ -1892,7 +1892,7 @@ "@executable_path/Frameworks", "@executable_path/../../Frameworks", ); - MARKETING_VERSION = 2.6.8; + MARKETING_VERSION = 2.6.9; PRODUCT_BUNDLE_IDENTIFIER = gvh.MeshtasticClient.Widgets; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; @@ -1924,7 +1924,7 @@ "@executable_path/Frameworks", "@executable_path/../../Frameworks", ); - MARKETING_VERSION = 2.6.8; + MARKETING_VERSION = 2.6.9; PRODUCT_BUNDLE_IDENTIFIER = gvh.MeshtasticClient.Widgets; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; From 95e08e5a480be63ce82e5910d3f76cc1ea1f5b60 Mon Sep 17 00:00:00 2001 From: kanakonagiri Date: Mon, 23 Jun 2025 12:39:05 +0900 Subject: [PATCH 170/213] add: ja translation --- Localizable.xcstrings | 5946 ++++++++++++++++++++++++++++++++++++++++- 1 file changed, 5906 insertions(+), 40 deletions(-) diff --git a/Localizable.xcstrings b/Localizable.xcstrings index ea67b2a9..124ce540 100644 --- a/Localizable.xcstrings +++ b/Localizable.xcstrings @@ -12,6 +12,12 @@ "value" : "%@" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "\t%@" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -41,6 +47,12 @@ "value" : "%@" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : " %@" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -70,6 +82,12 @@ "value" : "%@%%" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : " %@%%" + } + }, "zh-Hant-TW" : { "stringUnit" : { "state" : "translated", @@ -87,6 +105,12 @@ "value" : ": %@" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : ": %@" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -116,6 +140,12 @@ "value" : ": %d" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : ": %d" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -145,6 +175,12 @@ "value" : "(Ri)definire il PIN_GPS_EN per la propria scheda." } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "ボード用のPIN_GPS_ENを(再)定義してください。" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -173,6 +209,12 @@ "value" : "%@" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "%@" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -208,6 +250,12 @@ "value" : "%1$@ - %2$@" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "%1$@ - %2$@" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -243,6 +291,12 @@ "value" : "%1$@ - %2$@ - %3$@" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "%1$@ - %2$@ - %3$@" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -278,6 +332,12 @@ "value" : "%1$@ - %2$@ Verso %3$@ Indietro" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "%1$@ - %2$@ 送信 %3$@ 受信" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -352,6 +412,12 @@ "value" : "%@ - Non inviato" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "%@ - 送信されませんでした" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -386,6 +452,12 @@ "value" : "%1$@ (%2$@)" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "%1$@ (%2$@)" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -421,6 +493,12 @@ "value" : "%1$@ %2$@" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "%1$@ %2$@" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -456,6 +534,12 @@ "value" : "%1$@ %2$lld" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "%1$@ %2$lld" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -491,6 +575,12 @@ "value" : "%@ via" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "%@ 離れた場所" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -519,6 +609,12 @@ "value" : "%@ può essere lungo fino a %@ byte." } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "%1$@ は最大 %2$@ バイトまで設定できます。" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -547,6 +643,12 @@ "value" : "%@ Canali?" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "%@ チャンネル?" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -575,6 +677,12 @@ "value" : "i dati di configurazione %@ sono stati richiesti attraverso il canale di amministrazione, ma non è stata fornita alcuna risposta dal nodo remoto." } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "%@ 設定データが管理チャンネル経由で要求されましたが、リモートノードからの応答がありません。" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -603,6 +711,12 @@ "value" : "%@ dB" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "%@ dB" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -649,6 +763,12 @@ "value" : "%@ Si prega di provare a connettersi nuovamente e di controllare attentamente il PIN." } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "%@ 再度接続を試行し、PINを慎重に確認してください。" + } + }, "pl" : { "stringUnit" : { "state" : "translated", @@ -707,6 +827,12 @@ "value" : "%@ L'applicazione si riconnette automaticamente alla radio preferita se torna nel raggio d'azione." } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "%@ 優先無線機が範囲内に戻った場合、アプリは自動的に再接続します。" + } + }, "pl" : { "stringUnit" : { "state" : "translated", @@ -765,6 +891,12 @@ "value" : "%@ Questo errore di solito non può essere risolto senza dimenticare il dispositivo sotto Impostazioni > Bluetooth e riconnettersi alla radio." } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "%@ このエラーは通常、設定 > Bluetooth でデバイスの登録を解除し、無線機に再接続しない限り修正できません。" + } + }, "pl" : { "stringUnit" : { "state" : "translated", @@ -811,6 +943,12 @@ "value" : "%1$@, %2$@" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "%1$@, %2$@" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -846,6 +984,12 @@ "value" : "%1$@: %2$lld / %3$lld" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "%1$@: %2$lld / %3$lld" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -875,6 +1019,12 @@ "value" : "%@%%" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "%@%%" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -903,6 +1053,12 @@ "value" : "%@°F" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "%@°F" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -931,6 +1087,12 @@ "value" : "%@mA" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "%@mA" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -953,6 +1115,12 @@ "value" : "%@V" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "%@V" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -975,6 +1143,12 @@ "value" : "%d" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "%d" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -1039,6 +1213,12 @@ } } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "%dホップ" + } + }, "sr" : { "variations" : { "plural" : { @@ -1097,6 +1277,12 @@ "value" : "%d%%" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "%d%%" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -1125,6 +1311,12 @@ "value" : "%lf" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "%lf" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -1153,6 +1345,12 @@ "value" : "%lld" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "%lld" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -1187,6 +1385,12 @@ "value" : "%lld o meno hops di distanza" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "%lldホップ以下" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -1209,6 +1413,12 @@ "value" : "%lld Letture Totale" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "計 %lld 件の読み取り値" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -1231,6 +1441,12 @@ "value" : "%lld Totale eventi di rilevamento" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "計 %lld 件の検出イベント" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -1253,6 +1469,12 @@ "value" : "%lld%%" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "%lld%%" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -1287,6 +1509,12 @@ "value" : "%llddb Potenza di trasmissione" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "%llddb送信電力" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -1321,6 +1549,12 @@ "value" : "%llddBm Potenza di trasmissione" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "%llddBm送信電力" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -1343,6 +1577,12 @@ "value" : "< 1%" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "< 1%" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -1371,6 +1611,12 @@ "value" : "🦕 Versione a fine vita 🦖 ☄️" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "🦕 サポート終了バージョン 🦖 ☄️" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -1386,10 +1632,24 @@ } }, "0" : { - + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "0" + } + } + } }, "1" : { - + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "1" + } + } + } }, "1 byte" : { "localizations" : { @@ -1399,6 +1659,12 @@ "value" : "1 byte" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "1バイト" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -1427,6 +1693,12 @@ "value" : "a 1 salto di distanza" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "1ホップ先" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -1455,6 +1727,12 @@ "value" : "2.4 Ghz" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "2.4GHz" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -1477,6 +1755,12 @@ "value" : "7" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "7" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -1505,6 +1789,12 @@ "value" : "8" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "8" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -1526,7 +1816,14 @@ } }, "12 Hour Clock" : { - + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "12時間表示" + } + } + } }, "25" : { "localizations" : { @@ -1536,6 +1833,12 @@ "value" : "25" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "25" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -1564,6 +1867,12 @@ "value" : "50" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "50" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -1592,6 +1901,12 @@ "value" : "75" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "75" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -1620,6 +1935,12 @@ "value" : "100" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "100" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -1648,6 +1969,12 @@ "value" : "128 bit" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "128 bit" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -1669,7 +1996,14 @@ } }, "180" : { - + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "180" + } + } + } }, "256 bit" : { "localizations" : { @@ -1679,6 +2013,12 @@ "value" : "256 bit" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "256 bit" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -1700,10 +2040,24 @@ } }, "A channel index of 0 indicates the primary channel where all broadcast packets are sent from." : { - + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "チャンネルインデックス0は、すべてのブロードキャストパケットが送信されるプライマリチャンネルを示します。" + } + } + } }, "A green lock means the channel is securely encrypted with either a 128 or 256 bit AES key." : { - + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "緑色の鍵は、チャンネルが128ビットまたは256ビットのAESキーで安全に暗号化されていることを意味します。" + } + } + } }, "A Meshtastic QR code contains the LoRa config and channel values needed for radios to communicate. You can share a complete channel configuration using the Replace Channels option, if you choose Add Channels your shared channels will be added to the channels on the receiving radio." : { "localizations" : { @@ -1731,6 +2085,12 @@ "value" : "Un codice QR Meshtastic contiene la configurazione LoRa e i valori dei canali necessari alle radio per comunicare. È possibile condividere una configurazione completa dei canali utilizzando l'opzione Sostituisci canali; se si sceglie Aggiungi canali, i canali condivisi verranno aggiunti ai canali della radio ricevente." } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "MeshtasticのQRコードには、無線機が通信するために必要なLoRa設定とチャンネル値が含まれています。「チャンネルを置換」オプションを使用して完全なチャンネル設定を共有できます。「チャンネルを追加」を選択した場合、共有チャンネルは受信側無線機のチャンネルに追加されます。" + } + }, "pl" : { "stringUnit" : { "state" : "translated", @@ -1764,7 +2124,14 @@ } }, "A red lock with a slash means the channel is not securely encrypted, it uses either no key at all or a 1 byte known key. Traffic on this channel is easily intercepted." : { - + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "斜線付きの赤い鍵は、チャンネルが安全に暗号化されていないことを意味し、キーが全くないか1バイトの既知キーを使用しています。このチャンネルのトラフィックは簡単に傍受されます。" + } + } + } }, "A Trace Route was sent, no response has been received." : { "localizations" : { @@ -1774,6 +2141,12 @@ "value" : "È stata inviata una rotta di tracciamento, ma non è stata ricevuta alcuna risposta." } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "Trace Route が送信されましたが、応答が受信されませんでした。" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -1796,6 +2169,12 @@ "value" : "Informazioni" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "概要" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -1818,6 +2197,12 @@ "value" : "Informazioni su Meshtastic" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "Meshtasticについて" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -1846,6 +2231,12 @@ "value" : "Precisione %@" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "精度 %@" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -1868,6 +2259,12 @@ "value" : "SNR Ack: %@ dB" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "応答SNR: %@ dB" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -1890,6 +2287,12 @@ "value" : "Tempo di risposta: %@" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "応答時間: %@" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -1930,6 +2333,12 @@ "value" : "Confermato" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "確認済み" + } + }, "pl" : { "stringUnit" : { "state" : "translated", @@ -1970,6 +2379,12 @@ "value" : "Confermato da un altro nodo" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "他のノードで確認済み" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -1998,6 +2413,12 @@ "value" : "Azioni" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "アクション" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -2026,6 +2447,12 @@ "value" : "Attivo" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "アクティブ" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -2054,6 +2481,12 @@ "value" : "Attività" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "アクティビティ" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -2094,6 +2527,12 @@ "value" : "Override ADC" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "ADC上書き" + } + }, "pl" : { "stringUnit" : { "state" : "translated", @@ -2128,6 +2567,12 @@ "value" : "Aggiungi canale" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "チャンネルを追加" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -2150,6 +2595,12 @@ "value" : "Aggiungi canali" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "チャンネルを追加" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -2166,6 +2617,12 @@ }, "Add Contact" : { "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "連絡先を追加" + } + }, "zh-Hant-TW" : { "stringUnit" : { "state" : "translated", @@ -2204,6 +2661,12 @@ "value" : "Aggiungi ai preferiti" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "お気に入りに追加" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -2226,6 +2689,12 @@ "value" : "Aiuto supplementare" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "追加のヘルプ" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -2254,6 +2723,12 @@ "value" : "Indirizzo" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "住所" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -2275,7 +2750,14 @@ } }, "Admin Keys" : { - + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "管理者キー" + } + } + } }, "Administration" : { "localizations" : { @@ -2285,6 +2767,12 @@ "value" : "Amministrazione" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "管理" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -2307,6 +2795,12 @@ }, "Administration Enabled" : { "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "管理機能が有効" + } + }, "zh-Hant-TW" : { "stringUnit" : { "state" : "translated", @@ -2323,6 +2817,12 @@ "value" : "Avanzato" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "上級" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -2351,6 +2851,12 @@ "value" : "Dispositivo avanzato GPS" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "高度なデバイスGPS" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -2379,6 +2885,12 @@ "value" : "Opzioni GPIO avanzate" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "高度なGPIOオプション" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -2407,6 +2919,12 @@ "value" : "Flags di posizione avanzati" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "高度な位置フラグ" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -2441,6 +2959,12 @@ "value" : "Dopo" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "後" + } + }, "se" : { "stringUnit" : { "state" : "translated", @@ -2480,6 +3004,18 @@ } } } + }, + "ja" : { + "variations" : { + "plural" : { + "other" : { + "stringUnit" : { + "state" : "translated", + "value" : "あと %lld 日" + } + } + } + } } } }, @@ -2509,6 +3045,12 @@ "value" : "Dopo il salvataggio dei valori di configurazione, il nodo si riavvia." } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "設定値の保存後、ノードは再起動します。" + } + }, "pl" : { "stringUnit" : { "state" : "translated", @@ -2555,6 +3097,12 @@ "value" : "Pomeriggio" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "午後" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -2595,6 +3143,12 @@ "value" : "Tempo di trasmissione" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "エアタイム" + } + }, "pl" : { "stringUnit" : { "state" : "translated", @@ -2635,6 +3189,12 @@ "value" : "Avviso" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "アラート" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -2657,6 +3217,12 @@ "value" : "Attiva il cicalino GPIO alla ricezione di una campana" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "ベル受信時にGPIOブザーでアラート" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -2685,6 +3251,12 @@ "value" : "Attiva il cicalino GPIO alla ricezione di un messaggio" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "メッセージ受信時にGPIOブザーでアラート" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -2707,6 +3279,12 @@ "value" : "Attiva la vibrazione GPIO alla ricezione di una campana" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "ベル受信時にGPIO振動モーターでアラート" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -2735,6 +3313,12 @@ "value" : "Attiva la vibrazione GPIO alla ricezione di un messaggio" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "メッセージ受信時にGPIO振動モーターでアラート" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -2757,6 +3341,12 @@ "value" : "Avvisa alla ricezione di una campana" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "ベル受信時にアラート" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -2785,6 +3375,12 @@ "value" : "Avvisa alla ricezione di un messaggio" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "メッセージ受信時にアラート" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -2813,6 +3409,12 @@ "value" : "Tutti" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "全て" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -2841,6 +3443,12 @@ "value" : "Consenti le richieste di posizione" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "位置要求を許可" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -2863,6 +3471,12 @@ "value" : "Alt" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "高度" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -2891,6 +3505,12 @@ "value" : "Altitudine" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "高度" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -2941,6 +3561,12 @@ "value" : "Altitudine Separazione geoidale" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "高度ジオイド分離" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -2963,6 +3589,12 @@ "value" : "L'altitudine è il livello medio del mare" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "高度は平均海面レベル" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -3003,6 +3635,12 @@ "value" : "Sempre acceso" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "常にオン" + } + }, "pl" : { "stringUnit" : { "state" : "translated", @@ -3049,6 +3687,12 @@ "value" : "Punta sempre verso nord" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "常に北を指す" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -3089,6 +3733,12 @@ "value" : "Illuminazione ambientale" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "環境照明" + } + }, "se" : { "stringUnit" : { "state" : "translated", @@ -3141,6 +3791,12 @@ "value" : "Configurazione dell'illuminazione ambientale" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "環境照明設定" + } + }, "se" : { "stringUnit" : { "state" : "translated", @@ -3187,6 +3843,12 @@ "value" : "Configurazione del modulo di illuminazione ambientale ricevuta: %@" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "アンビエントライトモジュール設定を受信しました: %@" + } + }, "pl" : { "stringUnit" : { "state" : "translated", @@ -3233,6 +3895,12 @@ "value" : "Una rete mesh open source, off-grid, decentralizzata, che funziona con radio a basso costo e a bassa potenza." } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "手頃な価格の低電力無線機で動作する、オープンソース、オフグリッド、分散型メッシュネットワーク。" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -3261,6 +3929,12 @@ "value" : "I messaggi persi saranno consegnati nuovamente." } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "見逃したメッセージは再配信されます。" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -3307,6 +3981,12 @@ "value" : "Dispositivo di messaggistica collegato all'app o indipendente." } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "アプリ接続または独立型メッセージングデバイス。" + } + }, "pl" : { "stringUnit" : { "state" : "translated", @@ -3347,6 +4027,12 @@ "value" : "Dati dell'applicazione" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "App データ" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -3375,6 +4061,12 @@ "value" : "File dell'applicazione" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "アプリファイル" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -3403,6 +4095,12 @@ "value" : "Impostazioni dell'app" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "アプリ設定" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -3431,6 +4129,12 @@ "value" : "Applicazioni Apple" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "Appleアプリ" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -3465,6 +4169,12 @@ "value" : "Posizione approssimativa" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "正確な位置" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -3487,6 +4197,12 @@ "value" : "Sei sicuro di voler cancellare questo messaggio?" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "このメッセージを削除してもよろしいですか?" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -3521,6 +4237,12 @@ "value" : "Siete sicuri di voler resettare il nodo?" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "ノードを工場出荷時設定にリセットしてもよろしいですか?" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -3567,6 +4289,12 @@ "value" : "Sei sicuro?" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "本当によろしいですか?" + } + }, "pl" : { "stringUnit" : { "state" : "translated", @@ -3607,6 +4335,12 @@ "value" : "Australia / Nuova Zelanda" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "オーストラリア / ニュージーランド" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -3629,6 +4363,12 @@ "value" : "Passa automaticamente alla pagina successiva sullo schermo come un carosello, in base all'intervallo specificato." } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "指定した間隔に基づいて、カルーセルのように画面の次のページに自動的に切り替わります。" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -3703,6 +4443,12 @@ "value" : "Radio disponibili" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "利用可能な無線機" + } + }, "pl" : { "stringUnit" : { "state" : "translated", @@ -3761,6 +4507,12 @@ "value" : "Indietro" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "戻る" + } + }, "pl" : { "stringUnit" : { "state" : "translated", @@ -3794,10 +4546,24 @@ } }, "Backup" : { - + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "バックアップ" + } + } + } }, "Backup your private key to your iCloud keychain." : { - + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "プライベートキーをiCloudキーチェーンにバックアップします。" + } + } + } }, "Bad" : { "localizations" : { @@ -3807,6 +4573,12 @@ "value" : "Pessimo" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "悪い" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -3841,6 +4613,12 @@ "value" : "Richiesta non valida" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "不正なリクエスト" + } + }, "pl" : { "stringUnit" : { "state" : "translated", @@ -3887,6 +4665,12 @@ "value" : "Larghezza di banda" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "帯域幅" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -3915,6 +4699,12 @@ "value" : "Bar" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "バー" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -3943,6 +4733,12 @@ "value" : "Serie Bar" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "バー系列" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -3971,6 +4767,12 @@ "value" : "Pressione barometrica" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "気圧" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -4011,6 +4813,12 @@ "value" : "Batteria" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "バッテリー" + } + }, "pl" : { "stringUnit" : { "state" : "translated", @@ -4070,6 +4878,12 @@ "value" : "Livello della batteria" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "バッテリーレベル" + } + }, "pl" : { "stringUnit" : { "state" : "translated", @@ -4111,6 +4925,12 @@ "value" : "Livello della batteria %" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "バッテリーレベル %" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -4139,6 +4959,12 @@ "value" : "Baud" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "ボーレート" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -4173,6 +4999,12 @@ "value" : "In bicicletta" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "サイクリング" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -4201,6 +5033,12 @@ "value" : "BLE" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "BLE" + } + }, "se" : { "stringUnit" : { "state" : "translated", @@ -4247,6 +5085,12 @@ "value" : "Nome BLE" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "BLE 名前" + } + }, "pl" : { "stringUnit" : { "state" : "translated", @@ -4338,7 +5182,14 @@ } }, "BLE RSSI %lld" : { - + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "BLE RSSI %lld" + } + } + } }, "BLE: %@" : { "localizations" : { @@ -4348,6 +5199,12 @@ "value" : "BLE: %@" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "BLE: %@" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -4394,6 +5251,12 @@ "value" : "Bluetooth" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "Bluetooth" + } + }, "pl" : { "stringUnit" : { "state" : "translated", @@ -4452,6 +5315,12 @@ "value" : "Configurazione Bluetooth" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "Bluetooth設定" + } + }, "pl" : { "stringUnit" : { "state" : "translated", @@ -4510,6 +5379,12 @@ "value" : "Configurazione Bluetooth ricevuta: %@" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "Bluetooth設定を受信しました: %@" + } + }, "pl" : { "stringUnit" : { "state" : "translated", @@ -4568,6 +5443,12 @@ "value" : "Il Bluetooth è spento" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "Bluetoothがオフです" + } + }, "pl" : { "stringUnit" : { "state" : "translated", @@ -4608,6 +5489,12 @@ "value" : "Intervallo di trasmissione" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "ブロードキャスト間隔" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -4654,6 +5541,12 @@ "value" : "Dà priorità alla trasmissione di pacchetti di posizione GPS." } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "GPS位置パケットを優先的にブロードキャストします。" + } + }, "pl" : { "stringUnit" : { "state" : "translated", @@ -4712,6 +5605,12 @@ "value" : "Trasmette regolarmente la posizione come messaggio al canale predefinito per aiutare il recupero del dispositivo." } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "デバイスの回復を支援するため、定期的に位置情報をデフォルトチャンネルにメッセージとしてブロードキャストします。" + } + }, "pl" : { "stringUnit" : { "state" : "translated", @@ -4770,6 +5669,12 @@ "value" : "Trasmette i pacchetti di telemetria come priorità." } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "テレメトリパケットを優先的にブロードキャストします。" + } + }, "pl" : { "stringUnit" : { "state" : "translated", @@ -4810,6 +5715,12 @@ "value" : "Pulsante GPIO" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "ボタンGPIO" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -4838,6 +5749,12 @@ "value" : "Acquista dispositivi completi" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "完成品無線機を購入" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -4866,6 +5783,12 @@ "value" : "Cicalino GPIO" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "ブザーGPIO" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -4922,6 +5845,12 @@ "value" : "Byte" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "バイト" + } + }, "pl" : { "stringUnit" : { "state" : "translated", @@ -4965,6 +5894,12 @@ "value" : "Segnale di chiamata" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "コールサイン" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -4993,6 +5928,12 @@ "value" : "Il nominativo non deve essere vuoto" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "コールサインは空にできません" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -5039,6 +5980,12 @@ "value" : "Annullamento" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "キャンセル" + } + }, "pl" : { "stringUnit" : { "state" : "translated", @@ -5091,6 +6038,12 @@ "value" : "Configurazione del modulo Canned Message ricevuta: %@" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "定型メッセージモジュール設定を受信しました: %@" + } + }, "pl" : { "stringUnit" : { "state" : "translated", @@ -5149,6 +6102,12 @@ "value" : "Messaggi in scatola" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "定型メッセージ" + } + }, "pl" : { "stringUnit" : { "state" : "translated", @@ -5207,6 +6166,12 @@ "value" : "Configurazione dei messaggi in scatola" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "定型メッセージ設定" + } + }, "pl" : { "stringUnit" : { "state" : "translated", @@ -5259,6 +6224,12 @@ "value" : "Messaggi in scatola Messaggi ricevuti per: %@" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "定型メッセージ受信対象: %@" + } + }, "pl" : { "stringUnit" : { "state" : "translated", @@ -5299,6 +6270,12 @@ "value" : "Intervallo del carosello" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "カルーセル間隔" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -5327,6 +6304,12 @@ "value" : "Categorie" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "カテゴリ" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -5361,6 +6344,12 @@ "value" : "Categoria" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "カテゴリ" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -5383,6 +6372,12 @@ "value" : "Corrente Ch1" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ch1 現在" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -5405,6 +6400,12 @@ "value" : "Tensione Ch1" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ch1 電圧" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -5427,6 +6428,12 @@ "value" : "Corrente Ch2" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ch2 現在" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -5449,6 +6456,12 @@ "value" : "Tensione Ch2" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ch2 電圧" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -5471,6 +6484,12 @@ "value" : "Corrente Ch3" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ch3 現在" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -5493,6 +6512,12 @@ "value" : "Tensione Ch3" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ch3 電圧" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -5533,6 +6558,12 @@ "value" : "Canale" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "チャンネル" + } + }, "pl" : { "stringUnit" : { "state" : "translated", @@ -5573,6 +6604,12 @@ "value" : "Canale 0 Incluso" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "チャンネル0を含む" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -5601,6 +6638,12 @@ "value" : "Canale 1" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "チャンネル1" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -5623,6 +6666,12 @@ "value" : "Canale 1 Incluso" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "チャンネル1を含む" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -5651,6 +6700,12 @@ "value" : "Canale 2" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "チャンネル2" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -5673,6 +6728,12 @@ "value" : "Canale 2 incluso" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "チャンネル2を含む" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -5701,6 +6762,12 @@ "value" : "Canale 3" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "チャンネル 3" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -5723,6 +6790,12 @@ "value" : "Canale 3 incluso" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "チャンネル 3を含む" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -5751,6 +6824,12 @@ "value" : "Canale 4 Incluso" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "チャンネル 4を含む" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -5779,6 +6858,12 @@ "value" : "Canale 5 Incluso" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "チャンネル 5を含む" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -5807,6 +6892,12 @@ "value" : "Canale 6 Incluso" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "チャンネル 6を含む" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -5835,6 +6926,12 @@ "value" : "Canale 7 Incluso" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "チャンネル 7を含む" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -5863,6 +6960,12 @@ "value" : "dettagli del canale" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "チャンネル詳細" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -5891,6 +6994,12 @@ "value" : "Nome del canale" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "チャンネル名" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -5919,6 +7028,12 @@ "value" : "Il numero del canale deve essere compreso tra 0 e 7." } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "チャンネル番号は0から7の間である必要があります。" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -5947,6 +7062,12 @@ "value" : "Ruolo del canale" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "チャンネル役割" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -5975,6 +7096,12 @@ "value" : "URL del canale" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "チャンネル URL" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -6089,6 +7216,12 @@ "value" : "Canali" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "チャンネル" + } + }, "pl" : { "stringUnit" : { "state" : "translated", @@ -6150,7 +7283,14 @@ } }, "Channels Help" : { - + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "チャンネルヘルプ" + } + } + } }, "Chart" : { "localizations" : { @@ -6160,6 +7300,12 @@ "value" : "Grafico" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "チャート" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -6188,6 +7334,12 @@ "value" : "CHG" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "充電中" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -6210,6 +7362,12 @@ "value" : "Cina" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "中国" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -6232,6 +7390,12 @@ "value" : "Svuota" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "クリア" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -6278,6 +7442,12 @@ "value" : "Cancella i dati dell'app" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "クリア App Data" + } + }, "pl" : { "stringUnit" : { "state" : "translated", @@ -6318,6 +7488,12 @@ "value" : "Cancella registro" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "ログクリア" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -6333,7 +7509,14 @@ } }, "Clear Stale Nodes" : { - + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "古いノードをクリア" + } + } + } }, "Client" : { "localizations" : { @@ -6343,6 +7526,12 @@ "value" : "Cliente" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "クライアント" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -6377,6 +7566,12 @@ "value" : "Cliente Nascosto" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "クライアント Hidden" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -6405,6 +7600,12 @@ "value" : "Storia del cliente" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "クライアント履歴" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -6433,6 +7634,12 @@ "value" : "Richiesta di cronologia clienti inviata" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "クライアント履歴リクエストを送信しました" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -6461,6 +7668,12 @@ "value" : "Cliente Muto" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "クライアント無音" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -6489,6 +7702,12 @@ "value" : "Opzioni del cliente" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "クライアントオプション" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -6517,6 +7736,12 @@ "value" : "Evento rotariano in senso orario" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "時計回りロータリーイベント" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -6563,6 +7788,12 @@ "value" : "Chiudere" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "閉じる" + } + }, "pl" : { "stringUnit" : { "state" : "translated", @@ -6603,6 +7834,12 @@ "value" : "Tasso di codifica" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "符号化率" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -6637,6 +7874,12 @@ "value" : "Colore" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "色" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -6665,6 +7908,12 @@ "value" : "Comunicare" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "通信中" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -6687,6 +7936,12 @@ "value" : "Supporto alla community" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "コミュニティサポート" + } + }, "zh-Hant-TW" : { "stringUnit" : { "state" : "translated", @@ -6703,6 +7958,12 @@ "value" : "Configurazione" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "設定" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -6737,6 +7998,12 @@ "value" : "Configurazione per: %@" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "%@ の設定" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -6759,6 +8026,12 @@ "value" : "Preset di configurazione" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "設定プリセット" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -6793,6 +8066,12 @@ "value" : "Configurare" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "設定する" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -6815,6 +8094,12 @@ "value" : "Conferma" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "確認" + } + }, "zh-Hant-TW" : { "stringUnit" : { "state" : "translated", @@ -6837,6 +8122,12 @@ "value" : "Collegarsi a un nodo" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "ノードに接続" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -6853,6 +8144,12 @@ }, "Connect to MQTT via Proxy" : { "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "プロキシ経由でMQTTに接続" + } + }, "zh-Hant-TW" : { "stringUnit" : { "state" : "translated", @@ -6869,6 +8166,12 @@ "value" : "Collegare alla nuova radio?" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "新しい無線機に接続しますか?" + } + }, "zh-Hant-TW" : { "stringUnit" : { "state" : "translated", @@ -6903,6 +8206,12 @@ "value" : "Bluetooth collegato" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "接続済み" + } + }, "pl" : { "stringUnit" : { "state" : "translated", @@ -6949,6 +8258,12 @@ "value" : "Nodo collegato %@" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "接続済みノード %@" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -6971,6 +8286,12 @@ "value" : "Dispositivo connesso" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "接続済み無線機" + } + }, "zh-Hant-TW" : { "stringUnit" : { "state" : "translated", @@ -7005,6 +8326,12 @@ "value" : "Collegamento. ." } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "接続中..." + } + }, "pl" : { "stringUnit" : { "state" : "translated", @@ -7045,6 +8372,12 @@ "value" : "La connessione a un nuovo dispositivo cancellerà tutti i dati dell'applicazione sul telefono." } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "新しい無線機に接続すると、電話上の全てのアプリデータがクリアされます。" + } + }, "zh-Hant-TW" : { "stringUnit" : { "state" : "translated", @@ -7067,6 +8400,12 @@ "value" : "Tentativo di connessione %lld di 10" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "接続試行 %lld / 10" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -7147,6 +8486,12 @@ }, "Consent to Share Unencrypted Node Data via MQTT" : { "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "MQTT経由での暗号化されていないノードデータの共有に同意" + } + }, "zh-Hant-TW" : { "stringUnit" : { "state" : "translated", @@ -7157,6 +8502,12 @@ }, "Contact URL" : { "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "連絡先URL" + } + }, "zh-Hant-TW" : { "stringUnit" : { "state" : "needs_review", @@ -7191,6 +8542,12 @@ "value" : "Contatti (%@)" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "連絡先s (%@)" + } + }, "pl" : { "stringUnit" : { "state" : "translated", @@ -7231,6 +8588,12 @@ "value" : "Tipo di controllo" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "Control タイプ" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -7259,6 +8622,12 @@ "value" : "Controlla il LED lampeggiante del dispositivo. Per la maggior parte dei dispositivi controlla uno dei 4 LED, mentre quelli di alimentazione e del GPS non sono controllabili." } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "デバイス上の点滅LEDを制御します。ほとんどのデバイスでは最大4つのLEDのうち1つを制御し、充電器とGPS LEDは制御できません。" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -7293,6 +8662,12 @@ "value" : "Inviluppo convesso" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "凸包" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -7321,6 +8696,12 @@ "value" : "Coordinate" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "座標" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -7349,6 +8730,12 @@ "value" : "Coordinate %@, %@" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "座標 %@, %@" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -7383,6 +8770,12 @@ "value" : "Coordinate:" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "座標:" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -7423,6 +8816,12 @@ "value" : "Copia" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "コピー" + } + }, "pl" : { "stringUnit" : { "state" : "translated", @@ -7469,6 +8868,12 @@ "value" : "Impossibile trovare il nodo" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "ノードが見つかりません" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -7497,6 +8902,12 @@ "value" : "Evento rotativo antiorario" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "反時計回りの回転イベント" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -7525,6 +8936,12 @@ "value" : "Creare un waypoint" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "ウェイポイントを作成" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -7553,6 +8970,12 @@ "value" : "Creato: %@" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "作成日時: %@" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -7575,6 +8998,12 @@ "value" : "Attuale" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "現在" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -7603,6 +9032,12 @@ "value" : "Versione attuale del firmware: %@" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "現在のファームウェアバージョン: %@" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -7637,6 +9072,12 @@ "value" : "Versione attuale del firmware: %@, Ultima versione del firmware: %@" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "現在のファームウェアバージョン: %@、最新のファームウェアバージョン: %@" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -7671,6 +9112,12 @@ "value" : "Attuale: %lld" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "現在: %lld" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -7743,6 +9190,12 @@ "value" : "Data" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "日付" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -7765,6 +9218,12 @@ "value" : "Debug" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "デバッグ" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -7799,6 +9258,12 @@ "value" : "Registri di debug" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "デバッグログ" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -7821,6 +9286,12 @@ "value" : "Registri di debug%@" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "デバッグログ%@" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -7861,6 +9332,12 @@ "value" : "Formato dei gradi decimali" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "十進度形式" + } + }, "pl" : { "stringUnit" : { "state" : "translated", @@ -7919,6 +9396,12 @@ "value" : "Predefinito" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "デフォルト" + } + }, "pl" : { "stringUnit" : { "state" : "translated", @@ -8005,6 +9488,12 @@ "value" : "Gradi Minuti Secondi" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "度分秒" + } + }, "pl" : { "stringUnit" : { "state" : "translated", @@ -8063,6 +9552,12 @@ "value" : "Cancellare" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "削除" + } + }, "pl" : { "stringUnit" : { "state" : "translated", @@ -8102,6 +9597,12 @@ "state" : "translated", "value" : "Cancellare tutte le configurazioni, le chiavi e le associazioni bluetooth?" } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "全ての設定、キー、BLEボンドを削除しますか?" + } } } }, @@ -8112,6 +9613,12 @@ "state" : "translated", "value" : "Cancellare tutte le configurazioni?" } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "全ての設定を削除しますか?" + } } } }, @@ -8175,6 +9682,12 @@ "value" : "Cancellare tutte le metriche dell'ambiente?" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "全ての環境メトリクスを削除しますか?" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -8203,6 +9716,12 @@ "value" : "Cancellare tutti i dati dei passeggeri?" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "全てのPAXデータを削除しますか?" + } + }, "se" : { "stringUnit" : { "state" : "translated", @@ -8231,6 +9750,12 @@ "value" : "Cancellare tutte le posizioni?" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "全ての位置データを削除しますか?" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -8253,6 +9778,12 @@ "value" : "Cancellare il messaggio" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "メッセージを削除" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -8275,6 +9806,12 @@ "value" : "Cancellare i messaggi" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "メッセージを削除" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -8303,6 +9840,12 @@ "value" : "Cancellare il nodo" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "ノードを削除" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -8331,6 +9874,12 @@ "value" : "Cancellare il nodo?" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "ノードを削除しますか?" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -8353,6 +9902,12 @@ "value" : "Cancellare le metriche di potenza?" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "電力メトリクスを削除しますか?" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -8375,6 +9930,12 @@ "value" : "Descrizione" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "説明" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -8397,6 +9958,12 @@ "value" : "La descrizione deve essere inferiore a 100 byte" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "説明は100バイト未満である必要があります" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -8425,6 +9992,12 @@ "value" : "Rilevamento" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "検出" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -8447,6 +10020,12 @@ "value" : "Evento di rilevamento" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "検出イベント" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -8488,6 +10067,12 @@ "value" : "Sensore di rilevamento" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "検出センサー" + } + }, "pl" : { "stringUnit" : { "state" : "translated", @@ -8540,6 +10125,12 @@ "value" : "Configurazione del sensore di rilevamento" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "検出センサー設定" + } + }, "se" : { "stringUnit" : { "state" : "translated", @@ -8574,6 +10165,12 @@ "value" : "Registro del sensore di rilevamento" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "検出センサーログ" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -8596,6 +10193,12 @@ "value" : "I messaggi del sensore di rilevamento vengono ricevuti come messaggi di testo. Se si attivano le notifiche, si riceverà una notifica per ogni messaggio di rilevamento ricevuto e un badge per il messaggio non letto corrispondente." } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "検出センサーメッセージはテキストメッセージとして受信されます。通知を有効にすると、受信した各検出メッセージの通知と対応する未読メッセージバッジが表示されます。" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -8636,6 +10239,12 @@ "value" : "Configurazione del modulo sensore di rilevamento ricevuta: %@" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "検出センサーモジュール設定を受信しました: %@" + } + }, "pl" : { "stringUnit" : { "state" : "translated", @@ -8676,6 +10285,12 @@ "value" : "Sviluppatori" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "開発者" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -8722,6 +10337,12 @@ "value" : "Dispositivo" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "デバイス" + } + }, "pl" : { "stringUnit" : { "state" : "translated", @@ -8780,6 +10401,12 @@ "value" : "Configurazione del dispositivo" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "デバイス設定" + } + }, "pl" : { "stringUnit" : { "state" : "translated", @@ -8884,6 +10511,12 @@ "value" : "Configurazione del dispositivo" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "デバイス設定" + } + }, "se" : { "stringUnit" : { "state" : "translated", @@ -8924,6 +10557,12 @@ "value" : "Dispositivo GPS" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "デバイス GPS" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -8998,6 +10637,12 @@ "value" : "Metadati del dispositivo ricevuti da: %@" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "デバイスメタデータを受信: %@" + } + }, "pl" : { "stringUnit" : { "state" : "translated", @@ -9038,6 +10683,12 @@ "value" : "Metriche del dispositivo" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "デバイスメトリクス" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -9066,6 +10717,12 @@ "value" : "Registro delle metriche del dispositivo" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "デバイスメトリクスログ" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -9100,6 +10757,12 @@ "value" : "Modello dispositivo: %@" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "デバイスモデル: %@" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -9128,6 +10791,12 @@ "value" : "Ruolo del dispositivo" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "デバイス役割" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -9156,6 +10825,12 @@ "value" : "Schermata del dispositivo" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "デバイス画面" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -9202,6 +10877,12 @@ "value" : "Dispositivo che non inoltra pacchetti da altri dispositivi." } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "他のデバイスからのパケットを転送しないデバイス。" + } + }, "pl" : { "stringUnit" : { "state" : "translated", @@ -9260,6 +10941,12 @@ "value" : "Dispositivo che trasmette solo quando è necessario, per non dare nell'occhio o per risparmiare energia." } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "ステルス性や省電力のために必要な時にのみブロードキャストするデバイス。" + } + }, "pl" : { "stringUnit" : { "state" : "translated", @@ -9300,6 +10987,12 @@ "value" : "Diluizione della precisione (DOP) PDOP utilizzato per impostazione predefinita" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "精度希釈(DOP)、デフォルトでPDOPを使用" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -9328,6 +11021,12 @@ "value" : "Diretto" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "ダイレクト" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -9356,6 +11055,12 @@ "value" : "Aiuto per i messaggi diretti" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "ダイレクトメッセージヘルプ" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -9377,7 +11082,14 @@ } }, "Direct Message Key" : { - + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "ダイレクトメッセージキー" + } + } + } }, "Direct Messages" : { "localizations" : { @@ -9405,6 +11117,12 @@ "value" : "Messaggi diretti" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "ダイレクトメッセージ" + } + }, "pl" : { "stringUnit" : { "state" : "translated", @@ -9445,6 +11163,12 @@ "value" : "I messaggi diretti utilizzano la nuova infrastruttura a chiave pubblica per la crittografia. Richiede la versione firmware 2.5 o superiore." } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "ダイレクトメッセージは暗号化のために新しい公開鍵インフラストラクチャを使用しています。ファームウェアバージョン2.5以上が必要です。" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -9473,6 +11197,12 @@ "value" : "I messaggi diretti utilizzano la chiave condivisa del canale." } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "ダイレクトメッセージはチャンネルの共有キーを使用しています。" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -9519,6 +11249,12 @@ "value" : "Disattivato" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "無効" + } + }, "pl" : { "stringUnit" : { "state" : "translated", @@ -9577,6 +11313,12 @@ "value" : "Disconnessione" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "切断" + } + }, "pl" : { "stringUnit" : { "state" : "translated", @@ -9610,10 +11352,24 @@ } }, "Disconnect Node" : { - + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "ノードを切断" + } + } + } }, "Disconnect the currently connected node" : { - + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "現在接続中のノードを切断します" + } + } + } }, "Dismiss" : { "localizations" : { @@ -9641,6 +11397,12 @@ "value" : "Sospendere" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "閉じる" + } + }, "pl" : { "stringUnit" : { "state" : "translated", @@ -9699,6 +11461,12 @@ "value" : "Display" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "ディスプレイ" + } + }, "pl" : { "stringUnit" : { "state" : "translated", @@ -9751,6 +11519,12 @@ "value" : "Configurazione del display" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "ディスプレイ設定" + } + }, "pl" : { "stringUnit" : { "state" : "translated", @@ -9939,6 +11713,12 @@ "value" : "Distanza" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "距離" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -9967,6 +11747,12 @@ "value" : "Documentazione" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "ドキュメント" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -9989,6 +11775,12 @@ }, "Done" : { "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "完了" + } + }, "zh-Hant-TW" : { "stringUnit" : { "state" : "translated", @@ -10005,6 +11797,12 @@ "value" : "Doppio tocco come pulsante" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "ダブルタップをボタンとして使用" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -10051,6 +11849,12 @@ "value" : "In basso" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "下" + } + }, "pl" : { "stringUnit" : { "state" : "translated", @@ -10091,6 +11895,12 @@ "value" : "Downlink abilitato" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "ダウンリンク有効" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -10119,6 +11929,12 @@ "value" : "Aggiornamento del firmware con il drag & drop" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "ドラッグ&ドロップファームウェア更新" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -10147,6 +11963,12 @@ "value" : "Documentazione sull'aggiornamento del firmware con il drag & drop" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "ドラッグ&ドロップファームウェア更新ドキュメント" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -10209,6 +12031,12 @@ "value" : "Guida" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "運転" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -10237,6 +12065,12 @@ "value" : "Spillo in Mappe" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "マップにピンを配置" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -10271,6 +12105,12 @@ "value" : "Eco" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "エコー" + } + }, "se" : { "stringUnit" : { "state" : "translated", @@ -10305,6 +12145,12 @@ "value" : "Modifica di Waypoint" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "ウェイポイント編集" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -10345,6 +12191,12 @@ "value" : "Diciotto ore" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "18時間" + } + }, "pl" : { "stringUnit" : { "state" : "translated", @@ -10385,6 +12237,12 @@ "value" : "Elev. Guadagno" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "標高ゲイン" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -10407,6 +12265,12 @@ "value" : "Emoji" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "絵文字" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -10435,6 +12299,12 @@ "value" : "Vuoto" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "空" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -10463,6 +12333,12 @@ "value" : "Abilita la trasmissione di pacchetti via UDP sulla rete locale." } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "ローカルネットワーク上でUDP経由のパケットブロードキャストを有効にします。" + } + }, "zh-Hant-TW" : { "stringUnit" : { "state" : "translated", @@ -10479,6 +12355,12 @@ "value" : "Abilita le notifiche" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "通知を有効にする" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -10507,6 +12389,12 @@ "value" : "Abilita questo dispositivo come server Store and Forward. Richiede un dispositivo ESP32 con PSRAM." } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "このデバイスをStore and Forwardサーバーとして有効にします。PSRAMを搭載したESP32デバイスが必要です。" + } + }, "zh-Hant-TW" : { "stringUnit" : { "state" : "translated", @@ -10541,6 +12429,12 @@ "value" : "Abilitato" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "有効" + } + }, "pl" : { "stringUnit" : { "state" : "translated", @@ -10599,6 +12493,12 @@ "value" : "Abilita le trasmissioni automatiche di TAK PLI e riduce le trasmissioni di routine." } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "自動TAK PLIブロードキャストを有効にし、定期ブロードキャストを削減します。" + } + }, "pl" : { "stringUnit" : { "state" : "translated", @@ -10639,6 +12539,12 @@ "value" : "Consente ai dispositivi con uscita audio I2S nativa di utilizzare l'RTTTL tramite altoparlante come un cicalino. T-Watch S3 e T-Deck, ad esempio, dispongono di questa funzionalità." } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "ネイティブI2Sオーディオ出力を持つデバイスで、ブザーのようにスピーカー経由でRTTTLを使用できるようにします。例えば、T-Watch S3やT-Deckにはこの機能があります。" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -10667,6 +12573,12 @@ "value" : "Abilita il modulo del sensore di rilevamento; deve essere abilitato sia sul nodo con il sensore, sia su tutti i nodi che si desidera ricevere messaggi di testo del sensore di rilevamento o visualizzare il registro e il grafico del sensore di rilevamento." } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "検出センサーモジュールを有効にします。センサーを持つノードと、検出センサーテキストメッセージを受信したり、検出センサーログやチャートを表示したいノードの両方で有効にする必要があります。" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -10695,6 +12607,12 @@ "value" : "Abilita il modulo Salva & Inoltra." } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "Store and Forwardモジュールを有効にします。" + } + }, "zh-Hant-TW" : { "stringUnit" : { "state" : "translated", @@ -10710,6 +12628,12 @@ "state" : "translated", "value" : "Abilitando l'Ethernet verrà disabilita la connessione bluetooth all'applicazione. La connessione a nodi TCP non è disponibile su dispositivi Apple." } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ethernetを有効にすると、アプリへのBluetooth接続が無効になります。AppleデバイスではTCPノード接続は利用できません。" + } } } }, @@ -10720,6 +12644,12 @@ "state" : "translated", "value" : "L'attivazione del WiFi disabilita la connessione bluetooth all'applicazione. La connessione a nodi TCP non è disponibile su dispositivi Apple." } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "WiFiを有効にすると、アプリへのBluetooth接続が無効になります。AppleデバイスではTCPノード接続は利用できません。" + } } } }, @@ -10731,6 +12661,12 @@ "value" : "Evento di pressione dell'encoder" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "エンコーダープレスイベント" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -10771,6 +12707,12 @@ "value" : "Crittografato" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "暗号化済み" + } + }, "pl" : { "stringUnit" : { "state" : "translated", @@ -10817,6 +12759,12 @@ "value" : "Invio crittografato fallito" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "暗号化送信失敗" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -10839,6 +12787,12 @@ "value" : "Crittografia abilitata" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "暗号化有効" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -10873,6 +12827,12 @@ "value" : "Entrare in modalità DFU" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "DFUモードに入る" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -10901,6 +12861,12 @@ "value" : "ambiente" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "環境" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -10935,6 +12901,12 @@ "value" : "Ambiente" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "環境" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -10963,6 +12935,12 @@ "value" : "Metriche dei sensori" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "環境メトリクス" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -10991,6 +12969,12 @@ "value" : "Registro delle metriche ambientali" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "環境メトリクスログ" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -11025,6 +13009,12 @@ "value" : "Cancellare tutti i dati delle app?" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "全てのアプリデータを消去しますか?" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -11059,6 +13049,12 @@ "value" : "Cancellare tutti i dati del dispositivo e delle app?" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "全てのデバイスおよびアプリデータを消去しますか?" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -11087,6 +13083,12 @@ "value" : "Errore: %@" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "エラー: %@" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -11115,6 +13117,12 @@ "value" : "L'aggiornamento OTA di ESP 32 è in corso, fare clic sul pulsante qui sotto per inviare al dispositivo un messaggio di riavvio in amministrazione ota." } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "ESP32 OTAアップデートは開発中です。下のボタンをクリックして、デバイスにOTA管理モードへの再起動メッセージを送信してください。" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -11130,7 +13138,7 @@ "zh-Hant-TW" : { "stringUnit" : { "state" : "translated", - "value" : "ESP32 的 OTA 更新功能尚在開發中,請點擊下方按鈕以傳送重新啟動至 OTA 管理模式的訊息至您的裝置。" + "value" : "ESP32 の OTA 更新功能尚在開發中,請點擊下方按鈕以傳送重新啟動至 OTA 管理模式的訊息至您的裝置。" } } } @@ -11143,6 +13151,12 @@ "value" : "Aggiornamento del firmware del dispositivo ESP32" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "ESP32デバイスファームウェア更新" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -11171,6 +13185,12 @@ "value" : "Opzioni Ethernet" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "イーサネットオプション" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -11193,6 +13213,12 @@ "value" : "Unione Europea 433MHz" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "欧州連合 433MHz" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -11215,6 +13241,12 @@ "value" : "Unione Europea 868MHz" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "欧州連合 868MHz" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -11243,6 +13275,12 @@ "value" : "Sera" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "夕方" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -11265,6 +13303,12 @@ "value" : "Scambio di posizioni" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "位置交換" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -11305,6 +13349,12 @@ "value" : "Esclamativo" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "感嘆符" + } + }, "pl" : { "stringUnit" : { "state" : "translated", @@ -11338,7 +13388,14 @@ } }, "Expiration" : { - + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "有効期限" + } + } + } }, "Expire" : { "localizations" : { @@ -11348,6 +13405,12 @@ "value" : "Scadenza" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "期限切れ" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -11370,6 +13433,12 @@ "value" : "Scadenza" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "有効期限" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -11392,6 +13461,12 @@ "value" : "Scadenza: %@" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "有効期限: %@" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -11414,6 +13489,12 @@ "value" : "Esportazione" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "エクスポート" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -11454,6 +13535,12 @@ "value" : "Notifica esterna" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "外部通知" + } + }, "pl" : { "stringUnit" : { "state" : "translated", @@ -11512,6 +13599,12 @@ "value" : "Configurazione della notifica esterna" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "外部通知設定" + } + }, "pl" : { "stringUnit" : { "state" : "translated", @@ -11610,6 +13703,12 @@ "value" : "Reset di fabbrica" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "Factory リセット" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -11631,6 +13730,12 @@ "state" : "translated", "value" : "Il ripristino alle impostazioni di fabbrica eliminerà i dati del dispositivo e della app." } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "工場出荷時リセットによりデバイスとアプリのデータが削除されます。" + } } } }, @@ -11642,6 +13747,12 @@ "value" : "Impossibile codificare il contenuto del messaggio" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "メッセージ内容のエンコードに失敗しました" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -11664,6 +13775,12 @@ "value" : "Impossibile ottenere una posizione valida per lo scambio" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "交換用の有効な位置の取得に失敗しました" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -11686,6 +13803,12 @@ "value" : "Impossibile ottenere una posizione valida per lo scambio." } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "交換用の有効な位置の取得に失敗しました。" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -11714,6 +13837,12 @@ "value" : "Discreto" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "普通" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -11742,6 +13871,12 @@ "value" : "Preferito" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "お気に入り" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -11757,7 +13892,14 @@ } }, "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." : { - + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "お気に入りと無視されたノードは常に保持されます。PKCキーを持たないノードは、ユーザーが設定したスケジュールでアプリデータベースからクリアされ、PKCキーを持つノードは間隔が7日以上に設定されている場合のみクリアされます。この機能は、デバイスノードデータベースに保存されていないノードのみをアプリから削除します。" + } + } + } }, "Favorites" : { "localizations" : { @@ -11773,6 +13915,12 @@ "value" : "Preferiti" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "お気に入り" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -11795,6 +13943,12 @@ "value" : "I preferiti e i nodi con messaggi recenti appaiono in cima all'elenco dei contatti." } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "お気に入りと最近のメッセージがあるノードは、連絡先リストの上部に表示されます。" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -11829,6 +13983,12 @@ "value" : "Recuperare l'ultima posizione di un nodo cetaneo" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "特定のノードの最新位置を取得" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -11851,6 +14011,12 @@ "value" : "Quindici minuti" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "15分" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -11891,6 +14057,12 @@ "value" : "Quindici secondi" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "15秒" + } + }, "pl" : { "stringUnit" : { "state" : "translated", @@ -11931,6 +14103,12 @@ "value" : "Archiviazione dei file" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "ファイルストレージ" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -11959,6 +14137,12 @@ "value" : "Trova un contatto" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "連絡先を検索" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -11987,6 +14171,12 @@ "value" : "Trovare un nodo" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "ノードを検索" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -12027,6 +14217,12 @@ "value" : "Fine" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "完了" + } + }, "pl" : { "stringUnit" : { "state" : "translated", @@ -12073,6 +14269,12 @@ "value" : "Firmware" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "ファームウェア" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -12101,6 +14303,12 @@ "value" : "Documentazione sull'aggiornamento del firmware" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "ファームウェア更新ドキュメント" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -12135,6 +14343,12 @@ "value" : "Aggiornamenti del firmware" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "ファームウェア更新" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -12181,6 +14395,12 @@ "value" : "Versione del firmware" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "ファームウェアバージョン" + } + }, "pl" : { "stringUnit" : { "state" : "translated", @@ -12221,6 +14441,12 @@ "value" : "Sentito per la prima volta" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "初回受信" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -12261,6 +14487,12 @@ "value" : "Cinque ore" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "5時間" + } + }, "pl" : { "stringUnit" : { "state" : "translated", @@ -12307,6 +14539,12 @@ "value" : "Cinque minuti" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "5分" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -12347,6 +14585,12 @@ "value" : "Cinque secondi" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "5秒" + } + }, "pl" : { "stringUnit" : { "state" : "translated", @@ -12405,6 +14649,12 @@ "value" : "PIN fisso" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "固定ピン" + } + }, "pl" : { "stringUnit" : { "state" : "translated", @@ -12445,6 +14695,12 @@ "value" : "Posizione fissa" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "固定位置" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -12467,6 +14723,12 @@ "value" : "Schermo ribaltabile" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "画面反転" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -12489,6 +14751,12 @@ "value" : "Capovolgere lo schermo in verticale" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "画面を垂直に反転" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -12529,6 +14797,12 @@ "value" : "Segui" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "追従" + } + }, "pl" : { "stringUnit" : { "state" : "translated", @@ -12587,6 +14861,12 @@ "value" : "Seguire la direzione" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "方位で追従" + } + }, "pl" : { "stringUnit" : { "state" : "translated", @@ -12627,6 +14907,12 @@ "value" : "Per tutte le funzionalità Mqtt diverse dal rapporto sulle mappe, è necessario impostare anche l'uplink e il downlink per ogni canale che si desidera collegare tramite Mqtt." } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "マップレポート以外のすべてのMqtt機能については、Mqtt経由でブリッジしたい各チャンネルのアップリンクとダウンリンクも設定する必要があります。" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -12661,6 +14947,12 @@ "value" : "Per tutti" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "すべての人に" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -12689,6 +14981,12 @@ "value" : "Per me" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "自分に" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -12729,6 +15027,12 @@ "value" : "Quarantotto ore" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "48時間" + } + }, "pl" : { "stringUnit" : { "state" : "translated", @@ -12787,6 +15091,12 @@ "value" : "Quarantacinque secondi" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "45秒" + } + }, "pl" : { "stringUnit" : { "state" : "translated", @@ -12845,6 +15155,12 @@ "value" : "Quattro ore" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "4時間" + } + }, "pl" : { "stringUnit" : { "state" : "translated", @@ -12903,6 +15219,12 @@ "value" : "Quattro secondi" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "4秒" + } + }, "pl" : { "stringUnit" : { "state" : "translated", @@ -12949,6 +15271,12 @@ "value" : "Frequenza" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "周波数" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -12971,6 +15299,12 @@ "value" : "Override di frequenza" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "周波数オーバーライド" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -12993,6 +15327,12 @@ "value" : "Slot di frequenza" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "周波数スロット" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -13015,6 +15355,12 @@ "value" : "Nome semplificato" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "フレンドリー名" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -13043,6 +15389,12 @@ "value" : "Nome amichevole usato per formattare il messaggio inviato alla rete. Esempio: Il nome \"Movimento\" si tradurrebbe nel messaggio \"Movimento rilevato\"" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "メッシュに送信されるメッセージのフォーマットに使用されるフレンドリ名。例:「Motion」という名前は「Motion detected」というメッセージになります。" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -13071,6 +15423,12 @@ "value" : "Supporto completo" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "完全サポート" + } + }, "zh-Hant-TW" : { "stringUnit" : { "state" : "translated", @@ -13080,7 +15438,14 @@ } }, "Generate a new private key to replace the one currently in use. The public key will automatically be regenerated from your private key." : { - + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "現在使用中のプライベートキーを置き換える新しいプライベートキーを生成します。パブリックキーはプライベートキーから自動的に再生成されます。" + } + } + } }, "Generate QR Code" : { "localizations" : { @@ -13108,6 +15473,12 @@ "value" : "Generare un codice QR" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "QRコード生成" + } + }, "pl" : { "stringUnit" : { "state" : "translated", @@ -13141,7 +15512,14 @@ } }, "Generated from your public key and sent out to other nodes on the mesh to allow them to compute a shared secret key." : { - + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "パブリックキーから生成され、メッシュ上の他のノードに送信されて、共有秘密キーの計算を可能にします。" + } + } + } }, "Get custom waterproof solar and detection sensor router nodes, aluminium desktop nodes and rugged handsets." : { "localizations" : { @@ -13151,6 +15529,12 @@ "value" : "Nodi router con sensori solari e di rilevamento personalizzati e impermeabili, nodi da tavolo in alluminio e portatili robusti." } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "カスタム防水ソーラー・検出センサールーターノード、アルミニウムデスクトップノード、頑丈なハンドセットを入手できます。" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -13179,6 +15563,12 @@ "value" : "Ottenere la posizione del nodo" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "ノード位置取得" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -13207,6 +15597,12 @@ "value" : "Scarica NRF DFU dall'App Store" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "App StoreからNRF DFUを取得" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -13235,6 +15631,12 @@ "value" : "Ottenere l'ultimo firmware alfa" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "最新のアルファ版ファームウェアを取得" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -13263,6 +15665,12 @@ "value" : "Ottenere l'ultimo firmware stabile" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "最新の安定版ファームウェアを取得" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -13284,7 +15692,14 @@ } }, "GitHub Repository" : { - + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "GitHubリポジトリ" + } + } + } }, "Good" : { "localizations" : { @@ -13294,6 +15709,12 @@ "value" : "Buono" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "良好" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -13316,6 +15737,12 @@ "value" : "GPIO" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "GPIO" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -13344,6 +15771,12 @@ "value" : "Durata dell'uscita GPIO" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "GPIO出力時間" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -13366,6 +15799,12 @@ "value" : "Pin GPIO per la porta A dell'encoder rotativo." } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "ロータリーエンコーダーAポート用GPIOピン。" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -13388,6 +15827,12 @@ "value" : "Pin GPIO per la porta B dell'encoder rotativo." } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "ロータリーエンコーダーBポート用GPIOピン。" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -13410,6 +15855,12 @@ "value" : "Pin GPIO per encoder rotativo Porta di stampa." } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "ロータリーエンコーダープレスポート用GPIOピン。" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -13432,6 +15883,12 @@ "value" : "Pin GPIO da monitorare" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "GPIO ピン to monitor" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -13454,6 +15911,12 @@ "value" : "GPS IT GPIO" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "GPS有効化GPIO" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -13482,6 +15945,12 @@ "value" : "Formato GPS" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "GPS形式" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -13510,6 +15979,12 @@ "value" : "Ricezione GPS GPIO" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "GPS受信GPIO" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -13538,6 +16013,12 @@ "value" : "Trasmissione GPS GPIO" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "GPS送信GPIO" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -13572,6 +16053,12 @@ "value" : "Messaggio di gruppo" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "グループメッセージ" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -13600,6 +16087,12 @@ "value" : "Raffiche %@" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "突風 %@" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -13622,6 +16115,12 @@ "value" : "חחח" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "ハハ" + } + }, "se" : { "stringUnit" : { "state" : "translated", @@ -13656,6 +16155,12 @@ "value" : "Hardware" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "ハードウェア" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -13684,6 +16189,12 @@ "value" : "Pericoloso" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "危険" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -13706,6 +16217,12 @@ "value" : "Direzione" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "方位" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -13728,6 +16245,12 @@ "value" : "Direzione: %@" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "方位: %@" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -13768,6 +16291,12 @@ "value" : "Ascoltato" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "受信済み" + } + }, "pl" : { "stringUnit" : { "state" : "translated", @@ -13826,6 +16355,12 @@ "value" : "Cuore" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "ハート" + } + }, "pl" : { "stringUnit" : { "state" : "translated", @@ -13866,6 +16401,12 @@ "value" : "Nascondere gli avvisi" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "アラートを非表示" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -13888,6 +16429,12 @@ "value" : "Nascondi avvisi" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "アラートを非表示" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -13903,7 +16450,14 @@ } }, "Hide sidebar" : { - + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "サイドバーを隠す" + } + } + } }, "HIGH" : { "localizations" : { @@ -13919,6 +16473,12 @@ "value" : "ALTO" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "高" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -13953,6 +16513,12 @@ "value" : "Escursioni" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "ハイキング" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -13981,6 +16547,12 @@ "value" : "Storia Rendimento Max" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "履歴返信最大数" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -14003,6 +16575,12 @@ "value" : "Finestra di restituzione della cronologia" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "履歴返信時間枠" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -14031,6 +16609,12 @@ "value" : "Distanza in Hop" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "ホップ距離" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -14059,6 +16643,12 @@ "value" : "Luppolo lontano %d" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "ホップ距離 %d" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -14087,6 +16677,12 @@ "value" : "Via il luppolo:" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "ホップ距離:" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -14115,6 +16711,12 @@ "value" : "Luppolo in partenza: %d" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "ホップ距離: %d" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -14143,6 +16745,12 @@ "value" : "Ora" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "時間" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -14171,6 +16779,12 @@ "value" : "Ciclo di lavoro orario" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "時間あたりデューティサイクル" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -14193,6 +16807,12 @@ "value" : "Per quanto tempo lo schermo rimane acceso dopo la pressione del tasto utente o la ricezione di messaggi." } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "ユーザーボタンが押されたり、メッセージが受信された後、画面が点灯し続ける時間。" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -14221,6 +16841,12 @@ "value" : "Con quale frequenza vengono inviate le metriche del dispositivo attraverso la rete. L'impostazione predefinita è 30 minuti." } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "デバイスメトリクスがメッシュ経由で送信される頻度。デフォルトは30分です。" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -14305,6 +16931,12 @@ "value" : "Con quale frequenza dobbiamo cercare di ottenere una posizione GPS." } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "GPS位置を取得する頻度。" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -14373,6 +17005,12 @@ "value" : "Quanto spesso possiamo inviare un messaggio alla rete quando le persone vengono rilevate." } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "人が検出された時にメッシュにメッセージを送信する頻度。" + } + }, "pl" : { "stringUnit" : { "state" : "translated", @@ -14419,6 +17057,12 @@ "value" : "Come aggiornare il firmware" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "ファームウェアの更新方法" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -14447,6 +17091,12 @@ "value" : "Um" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "湿度" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -14475,6 +17125,12 @@ "value" : "Umidità" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "湿度" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -14509,6 +17165,12 @@ "value" : "Ibrido" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "ハイブリッド" + } + }, "pl" : { "stringUnit" : { "state" : "translated", @@ -14561,6 +17223,12 @@ "value" : "Flyover ibrido" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "ハイブリッド・フライオーバー" + } + }, "pl" : { "stringUnit" : { "state" : "translated", @@ -14595,6 +17263,12 @@ }, "I have read and understand the above. I voluntarily consent to the unencrypted transmission of my node data via MQTT." : { "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "上記を読み理解しました。MQTT経由でのノードデータの暗号化されない送信に自発的に同意します。" + } + }, "zh-Hant-TW" : { "stringUnit" : { "state" : "translated", @@ -14611,6 +17285,12 @@ "value" : "IAQ" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "IAQ" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -14639,6 +17319,12 @@ "value" : "IAQ " } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "IAQ " + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -14667,6 +17353,12 @@ "value" : "IAQ %lld" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "IAQ %lld" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -14695,6 +17387,12 @@ "value" : "Icona" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "アイコン" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -14723,6 +17421,12 @@ "value" : "Se è impostato DOP, utilizzare i valori HDOP / VDOP invece di PDOP" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "DOPが設定されている場合、PDOPの代わりにHDOP / VDOP値を使用します" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -14779,6 +17483,12 @@ "value" : "Se è difficile accedere al pulsante di ripristino del dispositivo, accedere alla modalità DFU." } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "デバイスのリセットボタンにアクセスが困難な場合は、ここでDFUモードに入ってください。" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -14807,6 +17517,12 @@ "value" : "Se è impostata, i pacchetti inviati saranno ritrasmessi al dispositivo." } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "設定すると、送信したパケットがデバイスにエコーバックされます。" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -14835,6 +17551,12 @@ "value" : "Se l'argomento predefinito della regione è troppo frequentato, è possibile scegliere un argomento più locale." } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "デフォルトの地域トピックが混雑している場合は、よりローカルなトピックを選択できます。" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -14863,6 +17585,12 @@ "value" : "Ignorare MQTT" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "MQTTを無視" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -14919,6 +17647,12 @@ "value" : "Ignorato" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "無視" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -14967,6 +17701,12 @@ "value" : "Percorso di importazione" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "ルートインポート" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -15013,6 +17753,12 @@ "value" : "Includere" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "含める" + } + }, "pl" : { "stringUnit" : { "state" : "translated", @@ -15065,6 +17811,12 @@ "value" : "Incompleto" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "未完了" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -15081,6 +17833,12 @@ }, "India" : { "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "インド" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -15103,6 +17861,12 @@ "value" : "Qualità dell'aria interna" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "室内空気品質" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -15131,6 +17895,12 @@ "value" : "Qualità dell'aria interna (IAQ)" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "室内空気質(IAQ)" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -15303,6 +18073,12 @@ "value" : "Ingressi" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "入力" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -15331,6 +18107,12 @@ "value" : "Barra superiore invertita per la visualizzazione a 2 colori" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "2色ディスプレイ用反転トップバー" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -15371,6 +18153,12 @@ "value" : "Emissione di Want Config a %@" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "%@ に設定要求を送信中" + } + }, "pl" : { "stringUnit" : { "state" : "translated", @@ -15411,6 +18199,12 @@ "value" : "Giappone" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "日本" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -15433,6 +18227,12 @@ "value" : "JSON abilitato" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "JSON有効" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -15461,6 +18261,12 @@ "value" : "La modalità JSON è un output MQTT limitato e non criptato per l'integrazione locale con l'assistente domestico" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "JSONモードは、Home Assistantとのローカル統合のための限定的で暗号化されていないMQTT出力です" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -15483,6 +18289,12 @@ }, "Jump to present" : { "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "最新に移動" + } + }, "zh-Hant-TW" : { "stringUnit" : { "state" : "translated", @@ -15505,6 +18317,12 @@ "value" : "Chiave" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "キー" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -15526,7 +18344,14 @@ } }, "Key Backup" : { - + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "キーバックアップ" + } + } + } }, "Key Mapping" : { "localizations" : { @@ -15536,6 +18361,12 @@ "value" : "Mappatura delle chiavi" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "キーマッピング" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -15564,6 +18395,12 @@ "value" : "Dimensione della chiave" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "キーサイズ" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -15586,6 +18423,12 @@ "value" : "Corea" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "韓国" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -15614,6 +18457,12 @@ "value" : "L'ultima volta che si è sentito" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "最終受信" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -15648,6 +18497,12 @@ "value" : "Latitudine" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "緯度" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -15669,10 +18524,24 @@ } }, "Latitude in degrees (e.g., 37.7749)" : { - + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "緯度(度単位、例: 37.7749)" + } + } + } }, "Latitude must be between -90 and 90 degrees" : { - + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "緯度は-90度から90度の間である必要があります" + } + } + } }, "LED Heartbeat" : { "localizations" : { @@ -15682,6 +18551,12 @@ "value" : "Battito cardiaco a LED" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "LEDハートビート" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -15710,6 +18585,12 @@ "value" : "Stato del LED" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "LED状態" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -15756,6 +18637,12 @@ "value" : "A sinistra" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "左" + } + }, "pl" : { "stringUnit" : { "state" : "translated", @@ -15814,6 +18701,12 @@ "value" : "Livello" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "レベル" + } + }, "pl" : { "stringUnit" : { "state" : "translated", @@ -15854,6 +18747,12 @@ "value" : "Operatore con licenza" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "ライセンスオペレーター" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -15882,6 +18781,12 @@ "value" : "Limitare tutti gli intervalli di trasmissione periodica, in particolare la telemetria e la posizione. Se è necessario aumentare gli hop, farlo sui nodi ai margini, non su quelli al centro. MQTT è sconsigliato quando il ciclo di lavoro è limitato, perché è il nodo gateway a svolgere tutto il lavoro." } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "特にテレメトリと位置情報のすべての定期ブロードキャスト間隔を制限します。ホップを増やす必要がある場合は、中央のノードではなく端のノードで行ってください。デューティサイクルが制限されている場合、ゲートウェイノードがすべての作業を行うため、MQTTは推奨されません。" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -15910,6 +18815,12 @@ "value" : "Serie di linee" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "線系列" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -15932,6 +18843,12 @@ "value" : "Caricamento dei log. . ." } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "Loading ログs. . ." + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -15966,6 +18883,12 @@ "value" : "Posizione:" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "場所:" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -16000,6 +18923,12 @@ "value" : "Bloccato" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "ロック済み" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -16028,6 +18957,12 @@ "value" : "Livelli del registro" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "ログレベル" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -16074,6 +19009,12 @@ "value" : "Registrazione" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "ログ記録" + } + }, "pl" : { "stringUnit" : { "state" : "translated", @@ -16114,6 +19055,12 @@ "value" : "Registri" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "ログ" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -16142,6 +19089,12 @@ "value" : "Registri:" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "ログ:" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -16176,6 +19129,12 @@ "value" : "Nome lungo" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "長い名前" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -16232,6 +19191,12 @@ "value" : "A lungo raggio - Veloce" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "長距離 - 高速" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -16254,6 +19219,12 @@ "value" : "Lungo raggio - Moderato" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "長距離 - 中程度" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -16276,6 +19247,12 @@ "value" : "Lungo raggio - Lento" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "長距離 - 低速" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -16304,6 +19281,12 @@ "value" : "Longitudine" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "経度" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -16325,10 +19308,24 @@ } }, "Longitude in degrees (e.g., -122.4194)" : { - + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "経度(度単位、例: -122.4194)" + } + } + } }, "Longitude must be between -180 and 180 degrees" : { - + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "経度は-180度から180度の間である必要があります" + } + } + } }, "LoRa" : { "localizations" : { @@ -16356,6 +19353,12 @@ "value" : "LoRa" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "LoRa" + } + }, "pl" : { "stringUnit" : { "state" : "translated", @@ -16414,6 +19417,12 @@ "value" : "Configurazione LoRa" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "LoRa設定" + } + }, "pl" : { "stringUnit" : { "state" : "translated", @@ -16518,6 +19527,12 @@ "value" : "Oggetti smarriti" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "落とし物" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -16546,6 +19561,12 @@ "value" : "BASSO" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "低" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -16608,6 +19629,12 @@ "value" : "Malesia 433MHz" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "マレーシア 433MHz" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -16630,6 +19657,12 @@ "value" : "Malesia 919MHz" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "マレーシア 919MHz" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -16670,6 +19703,12 @@ "value" : "Gestire i canali" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "チャンネル管理" + } + }, "pl" : { "stringUnit" : { "state" : "translated", @@ -16710,6 +19749,12 @@ "value" : "Dispositivo gestito" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "管理されたデバイス" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -16756,6 +19801,12 @@ "value" : "Configurazione manuale" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "手動設定" + } + }, "pl" : { "stringUnit" : { "state" : "translated", @@ -16802,6 +19853,12 @@ "value" : "Opzioni mappa" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "マップオプション" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -16830,6 +19887,12 @@ "value" : "Intervallo di pubblicazione della mappa" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "マップ公開間隔" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -16858,6 +19921,12 @@ "value" : "Rapporto sulla mappa" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "マップレポート" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -16904,6 +19973,12 @@ "value" : "Raggiunta la massima ritrasmissione" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "最大再送信回数に到達" + } + }, "pl" : { "stringUnit" : { "state" : "translated", @@ -16944,6 +20019,12 @@ "value" : "Medio raggio - Veloce" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "中距離 - 高速" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -16966,6 +20047,12 @@ "value" : "Medio raggio - Lento" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "中距離 - 低速" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -16988,6 +20075,12 @@ "value" : "Aggiornamento dell'attività di rete" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "メッシュアクティビティ更新" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -17028,6 +20121,12 @@ "value" : "Rete Attività live" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "メッシュライブアクティビティ" + } + }, "pl" : { "stringUnit" : { "state" : "translated", @@ -17086,6 +20185,12 @@ "value" : "Mappa della mesh" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "メッシュマップ" + } + }, "pl" : { "stringUnit" : { "state" : "translated", @@ -17132,6 +20237,12 @@ "value" : "Il Nodo Meshtastic %@ ha condiviso i canali con voi" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "Meshtasticノード %@ があなたとチャンネルを共有しました" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -17154,6 +20265,12 @@ "value" : "Meshtastic® Copyright Meshtastic LLC" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "Meshtastic® Copyright Meshtastic LLC" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -17182,6 +20299,12 @@ "value" : "Messaggio" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "メッセージ" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -17210,6 +20333,12 @@ "value" : "Il contenuto del messaggio supera i 200 byte." } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "メッセージ内容が200バイトを超えています。" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -17250,6 +20379,12 @@ "value" : "Dettagli del messaggio" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "メッセージ詳細" + } + }, "pl" : { "stringUnit" : { "state" : "translated", @@ -17366,6 +20501,12 @@ "value" : "Invio messaggio fallito, connessione non corretta a %@" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "メッセージ送信に失敗しました。%@ に適切に接続されていません" + } + }, "pl" : { "stringUnit" : { "state" : "translated", @@ -17399,7 +20540,15 @@ } }, "Message Size" : { - "comment" : "VoiceOver label for message size" + "comment" : "VoiceOver label for message size", + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "メッセージサイズ" + } + } + } }, "Message Status Options" : { "localizations" : { @@ -17409,6 +20558,12 @@ "value" : "Opzioni di stato del messaggio" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "メッセージ Status Options" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -17443,6 +20598,12 @@ "value" : "Messaggi" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "メッセージ" + } + }, "pl" : { "stringUnit" : { "state" : "translated", @@ -17489,6 +20650,12 @@ "value" : "I messaggi sono separati da |" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "メッセージs separate with |" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -17504,7 +20671,14 @@ } }, "Messaging" : { - + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "メッセージング" + } + } + } }, "Metric" : { "localizations" : { @@ -17514,6 +20688,12 @@ "value" : "Metrico" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "メトリック" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -17542,6 +20722,12 @@ "value" : "Mezzogiorno" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "正午" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -17582,6 +20768,12 @@ "value" : "Sistema di riferimento della griglia militare" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "軍用格子座標系" + } + }, "pl" : { "stringUnit" : { "state" : "translated", @@ -17628,6 +20820,12 @@ "value" : "Distanza minima" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "最小距離" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -17656,6 +20854,12 @@ "value" : "Intervallo minimo" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "最小間隔" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -17740,6 +20944,12 @@ "value" : "Modalità" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "モード" + } + }, "pl" : { "stringUnit" : { "state" : "translated", @@ -17780,6 +20990,12 @@ "value" : "Modello" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "モデル" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -17808,6 +21024,12 @@ "value" : "Moderato" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "中程度" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -17854,6 +21076,12 @@ "value" : "Configurazione del modulo" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "モジュール設定" + } + }, "pl" : { "stringUnit" : { "state" : "translated", @@ -17900,6 +21128,12 @@ "value" : "Mattina" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "朝" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -17980,6 +21214,12 @@ "value" : "MQTT" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "MQTT" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -18026,6 +21266,12 @@ "value" : "Proxy client MQTT" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "MQTTクライアントプロキシ" + } + }, "pl" : { "stringUnit" : { "state" : "translated", @@ -18084,6 +21330,12 @@ "value" : "Configurazione MQTT" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "MQTT設定" + } + }, "pl" : { "stringUnit" : { "state" : "translated", @@ -18142,6 +21394,12 @@ "value" : "Configurazione del modulo MQTT ricevuta: %@" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "MQTTモジュール設定を受信しました: %@" + } + }, "pl" : { "stringUnit" : { "state" : "translated", @@ -18194,6 +21452,12 @@ "value" : "Moltiplicatore" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "乗数" + } + }, "pl" : { "stringUnit" : { "state" : "translated", @@ -18228,6 +21492,12 @@ "value" : "Deve essere una singola emoji" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "単一の絵文字である必要があります" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -18268,6 +21538,12 @@ "value" : "MyInfo ricevuto: %@" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "マイ情報受信: %@" + } + }, "pl" : { "stringUnit" : { "state" : "translated", @@ -18308,6 +21584,12 @@ "value" : "Timeout di Nag" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "通知タイムアウト" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -18336,6 +21618,12 @@ "value" : "Nome" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "名前" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -18370,6 +21658,12 @@ "value" : "Il nome deve essere inferiore a 30 byte" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "名前は30バイト未満である必要があります" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -18398,6 +21692,12 @@ "value" : "Spostarsi sul nodo" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "ノードに移動" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -18420,6 +21720,12 @@ "value" : "Argomenti vicini" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "近くのトピック" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -18460,6 +21766,12 @@ "value" : "Rete" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "ネットワーク" + } + }, "pl" : { "stringUnit" : { "state" : "translated", @@ -18518,6 +21830,12 @@ "value" : "Configurazione della rete" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "ネットワーク設定" + } + }, "pl" : { "stringUnit" : { "state" : "translated", @@ -18576,6 +21894,12 @@ "value" : "Configurazione di rete ricevuta: %@" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "ネットワーク設定を受信しました: %@" + } + }, "pl" : { "stringUnit" : { "state" : "translated", @@ -18616,6 +21940,12 @@ "value" : "Stato della rete Arancione" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "ネットワーク状態 オレンジ" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -18644,6 +21974,12 @@ "value" : "Stato della rete Rosso" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "ネットワーク状態 レッド" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -18678,6 +22014,12 @@ "value" : "Mai" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "なし" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -18700,6 +22042,12 @@ "value" : "Nuovo nodo" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "新しいノード" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -18722,6 +22070,12 @@ "value" : "È stato scoperto un nuovo nodo" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "新しいノードが発見されました" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -18744,6 +22098,12 @@ "value" : "Nuova Zelanda 865MHz" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "ニュージーランド 865MHz" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -18772,6 +22132,12 @@ "value" : "È disponibile un firmware più recente" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "新しいファームウェアが利用可能です" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -18806,6 +22172,12 @@ "value" : "Notte" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "夜間" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -18846,6 +22218,12 @@ "value" : "Posizioni NMEA" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "NMEA位置情報" + } + }, "pl" : { "stringUnit" : { "state" : "translated", @@ -18904,6 +22282,12 @@ "value" : "Nessun canale" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "チャンネルなし" + } + }, "pl" : { "stringUnit" : { "state" : "translated", @@ -18950,6 +22334,12 @@ "value" : "Nessun nodo collegato" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "接続されたノードがありません" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -18990,6 +22380,12 @@ "value" : "Nessun dispositivo collegato" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "デバイスが接続されていません" + } + }, "pl" : { "stringUnit" : { "state" : "translated", @@ -19030,6 +22426,12 @@ "value" : "Nessuna metrica del dispositivo" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "デバイスメトリクスがありません" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -19052,6 +22454,12 @@ "value" : "Nessuna metrica ambientale" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "環境メトリクスがありません" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -19098,6 +22506,12 @@ "value" : "Nessuna interfaccia" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "インターフェースがありません" + } + }, "pl" : { "stringUnit" : { "state" : "translated", @@ -19138,6 +22552,12 @@ "value" : "Nessun registro del contatore PAX" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "PAXカウンターログがありません" + } + }, "se" : { "stringUnit" : { "state" : "translated", @@ -19184,6 +22604,12 @@ "value" : "Nessun PIN (funziona e basta)" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "PIN不要(自動接続)" + } + }, "pl" : { "stringUnit" : { "state" : "translated", @@ -19230,6 +22656,12 @@ "value" : "Nessuna posizione" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "位置情報がありません" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -19252,6 +22684,12 @@ "value" : "Nessuna metrica di potenza" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "電力メトリクスがありません" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -19292,6 +22730,12 @@ "value" : "Nessuna risposta" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "応答なし" + } + }, "pl" : { "stringUnit" : { "state" : "translated", @@ -19350,6 +22794,12 @@ "value" : "Nessun percorso" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "ルートなし" + } + }, "pl" : { "stringUnit" : { "state" : "translated", @@ -19396,6 +22846,12 @@ "value" : "Nodo" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "ノード" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -19458,6 +22914,12 @@ "value" : "Il nodo non ha posizioni" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "ノードに位置情報がありません" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -19486,6 +22948,12 @@ "value" : "Storia del nodo" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "Node 履歴" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -19508,6 +22976,12 @@ "value" : "Intervallo di trasmissione delle informazioni sul nodo" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "ノード情報ブロードキャスト間隔" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -19594,6 +23068,12 @@ "value" : "Mappa dei nodi" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "Node マップ" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -19662,6 +23142,12 @@ "value" : "Nodi" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "ノード" + } + }, "se" : { "stringUnit" : { "state" : "translated", @@ -19772,6 +23258,12 @@ "value" : "Nessuno" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "なし" + } + }, "pl" : { "stringUnit" : { "state" : "translated", @@ -19812,6 +23304,12 @@ "value" : "Non è un file di percorso valido" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "有効なルートファイルではありません" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -19852,6 +23350,12 @@ "value" : "Non autorizzato" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "未認証" + } + }, "pl" : { "stringUnit" : { "state" : "translated", @@ -19904,6 +23408,12 @@ "value" : "Non presente" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "存在しません" + } + }, "se" : { "stringUnit" : { "state" : "translated", @@ -19944,6 +23454,12 @@ "value" : "Note" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "メモ" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -19972,6 +23488,12 @@ "value" : "Numero di hop" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "ホップ数" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -20000,6 +23522,12 @@ "value" : "Numero di record" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "レコード数" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -20028,6 +23556,12 @@ "value" : "Numero di satelliti" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "衛星数" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -20068,6 +23602,12 @@ "value" : "Spento" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "オフ" + } + }, "pl" : { "stringUnit" : { "state" : "translated", @@ -20114,6 +23654,12 @@ "value" : "OK" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "OK" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -20136,6 +23682,12 @@ "value" : "Ok a MQTT" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "MQTT OK" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -20170,6 +23722,12 @@ "value" : "Tipo OLED" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "OLEDタイプ" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -20216,6 +23774,12 @@ "value" : "Solo all'avvio" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "起動時のみ" + } + }, "pl" : { "stringUnit" : { "state" : "translated", @@ -20302,6 +23866,12 @@ "value" : "Un'ora" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "1時間" + } + }, "pl" : { "stringUnit" : { "state" : "translated", @@ -20360,6 +23930,12 @@ "value" : "Un minuto" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "1分" + } + }, "pl" : { "stringUnit" : { "state" : "translated", @@ -20418,6 +23994,12 @@ "value" : "Un secondo" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "1秒" + } + }, "pl" : { "stringUnit" : { "state" : "translated", @@ -20464,6 +24046,12 @@ "value" : "In linea" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "オンライン" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -20490,6 +24078,12 @@ }, "Only rebroadcasts packets from the core portnums: NodeInfo, Text, Position, Telemetry, and Routing." : { "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "コアポート番号からのパケットのみ再ブロードキャスト: ノード情報、テキスト、位置、テレメトリ、ルーティング。" + } + }, "zh-Hant-TW" : { "stringUnit" : { "state" : "translated", @@ -20598,6 +24192,12 @@ "value" : "Ottimizzato per i display a 2 colori" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "2色ディスプレイ用に最適化" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -20638,6 +24238,12 @@ "value" : "Ottimizzato per la comunicazione del sistema ATAK, riduce le trasmissioni di routine." } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "ATAKシステム通信用に最適化、定期ブロードキャストを削減。" + } + }, "pl" : { "stringUnit" : { "state" : "translated", @@ -20706,6 +24312,12 @@ "value" : "GPIO opzionale" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "オプション GPIO" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -20746,6 +24358,12 @@ "value" : "Opzioni" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "オプション" + } + }, "pl" : { "stringUnit" : { "state" : "translated", @@ -20804,6 +24422,12 @@ "value" : "Riferimento di griglia Ordnance Survey" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "英国陸地測量部格子座標" + } + }, "pl" : { "stringUnit" : { "state" : "translated", @@ -20844,6 +24468,12 @@ "value" : "Dettagli della voce del registro OS" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "OSログエントリ詳細" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -20866,6 +24496,12 @@ "value" : "Gli aggiornamenti OTA non sono supportati da questo dispositivo NRF." } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "このNRFデバイスではOTA更新はサポートされていません。" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -20894,6 +24530,12 @@ "value" : "Gli aggiornamenti OTA non sono supportati dalla vostra piattaforma." } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "お使いのプラットフォームではOTA更新はサポートされていません。" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -20922,6 +24564,12 @@ "value" : "Altre fonti di dati" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "その他のデータソース" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -20950,6 +24598,12 @@ "value" : "Emissione di registrazioni di debug in tempo reale via seriale, visualizzazione ed esportazione di registri del dispositivo con correzione della posizione via Bluetooth." } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "シリアル経由でライブデバッグログを出力し、Bluetooth経由で位置情報を削除したデバイスログを表示・エクスポート。" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -20972,6 +24626,12 @@ "value" : "Pin di uscita cicalino GPIO " } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "出力ピンブザーGPIO" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -20994,6 +24654,12 @@ "value" : "Pin di uscita GPIO" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "出力ピンGPIO" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -21016,6 +24682,12 @@ "value" : "Pin di uscita vibra GPIO" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "出力ピン振動GPIO" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -21038,6 +24710,12 @@ "value" : "Overlanding" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "オーバーランド" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -21066,6 +24744,12 @@ "value" : "Annulla il rilevamento automatico dello schermo OLED." } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "自動OLEDスクリーン検出をオーバーライド。" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -21106,6 +24790,12 @@ "value" : "Modalità di associazione" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "ペアリングモード" + } + }, "pl" : { "stringUnit" : { "state" : "translated", @@ -21164,6 +24854,12 @@ "value" : "Password" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "パスワード" + } + }, "pl" : { "stringUnit" : { "state" : "translated", @@ -21222,6 +24918,12 @@ "value" : "Pausa" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "一時停止" + } + }, "pl" : { "stringUnit" : { "state" : "translated", @@ -21268,6 +24970,12 @@ "value" : "Contatore PAX" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "PAXカウンター" + } + }, "pl" : { "stringUnit" : { "state" : "translated", @@ -21314,6 +25022,12 @@ "value" : "Configurazione del contatore PAX" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "PAXカウンター設定" + } + }, "se" : { "stringUnit" : { "state" : "translated", @@ -21348,6 +25062,12 @@ "value" : "Configurazione del contatore PAX ricevuta: %@" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "PAXカウンター設定を受信しました: %@" + } + }, "se" : { "stringUnit" : { "state" : "translated", @@ -21376,6 +25096,12 @@ "value" : "Registro del contatore PAX" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "PAXカウンターログ" + } + }, "se" : { "stringUnit" : { "state" : "translated", @@ -21416,6 +25142,12 @@ "value" : "Messaggio del contatore PAX ricevuto da: %@" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "PAXカウンターメッセージを受信: %@" + } + }, "pl" : { "stringUnit" : { "state" : "translated", @@ -21450,6 +25182,12 @@ }, "paxcounter.log" : { "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "PAXカウンターログ" + } + }, "zh-Hant-TW" : { "stringUnit" : { "state" : "translated", @@ -21472,6 +25210,12 @@ "value" : "Eseguire un reset di fabbrica sul nodo a cui si è connessi" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "接続しているノードの工場出荷時リセットを実行" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -21494,6 +25238,12 @@ "value" : "Filippine 433MHz" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "フィリピン 433MHz" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -21516,6 +25266,12 @@ "value" : "Filippine 868MHz" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "フィリピン 868MHz" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -21538,6 +25294,12 @@ "value" : "Filippine 915MHz" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "フィリピン 915MHz" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -21578,6 +25340,12 @@ "value" : "Telefono GPS" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "スマートフォンGPS" + } + }, "pl" : { "stringUnit" : { "state" : "translated", @@ -21618,6 +25386,12 @@ "value" : "Pin %lld" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "ピン %lld" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -21640,6 +25414,12 @@ "value" : "Pin A" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "ピンA" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -21662,6 +25442,12 @@ "value" : "Pin B" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "ピンB" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -21690,6 +25476,12 @@ "value" : "Amministrazione dei nodi basata su PKI, richiede la versione firmware 2.5+" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "PKIベースのノード管理、ファームウェアバージョン2.5+が必要" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -21712,6 +25504,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" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "マップレポートは暗号化されていないため、あなたのデータが第三者によって永続的に保存・表示される可能性があることをご承知ください。Meshtasticは、このようなデータの保存、表示、開示について一切の責任を負いません。" + } + }, "zh-Hant-TW" : { "stringUnit" : { "state" : "translated", @@ -21728,6 +25526,12 @@ "value" : "Collegarsi a una radio per configurare le impostazioni." } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "設定を構成するには無線機に接続してください。" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -21756,6 +25560,12 @@ "value" : "Impostare una regione" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "地域を設定してください" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -21778,6 +25588,12 @@ "value" : "Punti di interesse" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "興味地点" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -21864,6 +25680,12 @@ "value" : "Posizione" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "位置" + } + }, "pl" : { "stringUnit" : { "state" : "translated", @@ -21916,6 +25738,12 @@ "value" : "Configurazione della posizione" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "位置設定" + } + }, "pl" : { "stringUnit" : { "state" : "translated", @@ -21974,6 +25802,12 @@ "value" : "Configurazione della posizione ricevuta: %@" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "位置設定を受信しました: %@" + } + }, "pl" : { "stringUnit" : { "state" : "translated", @@ -22008,6 +25842,12 @@ "value" : "Scambio di posizioni non riuscito" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "位置交換に失敗しました" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -22030,6 +25870,12 @@ "value" : "Scambio di posizioni richiesto" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "位置交換要求" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -22052,6 +25898,12 @@ "value" : "Bandiere di posizione" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "位置フラグ" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -22074,6 +25926,12 @@ "value" : "Registro di posizione" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "位置ログ" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -22118,6 +25976,12 @@ "value" : "Pacchetto posizione" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "位置パケット" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -22204,6 +26068,12 @@ "value" : "Posizione inviata" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "位置送信済み" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -22226,6 +26096,12 @@ "value" : "Posizioni abilitate" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "位置情報有効" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -22254,6 +26130,12 @@ "value" : "Le posizioni saranno fornite dal GPS del dispositivo; se si seleziona disabilitato o non presente, è possibile impostare una posizione fissa." } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "位置情報はデバイスのGPSによって提供されます。無効または存在しないを選択した場合は、固定位置を設定できます。" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -22294,6 +26176,12 @@ "value" : "Potenza" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "電源" + } + }, "pl" : { "stringUnit" : { "state" : "translated", @@ -22346,6 +26234,12 @@ "value" : "Configurazione dell'alimentazione" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "電源設定" + } + }, "pl" : { "stringUnit" : { "state" : "translated", @@ -22386,6 +26280,12 @@ "value" : "Configurazione dell'alimentazione ricevuta: %@" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "電源設定を受信しました: %@" + } + }, "se" : { "stringUnit" : { "state" : "translated", @@ -22414,6 +26314,12 @@ "value" : "Metriche di potenza" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "電源メトリクス" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -22436,6 +26342,12 @@ "value" : "Registro delle metriche di potenza" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "電力メトリクスログ" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -22458,6 +26370,12 @@ "value" : "Spegnimento" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "電源オフ" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -22480,6 +26398,12 @@ "value" : "Opzioni di alimentazione" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "電源オプション" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -22514,6 +26438,12 @@ "value" : "Risparmio energetico" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "省電力" + } + }, "pl" : { "stringUnit" : { "state" : "translated", @@ -22554,6 +26484,12 @@ "value" : "Schermo di alimentazione" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "電源画面" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -22582,6 +26518,12 @@ "value" : "Potenziato" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "電源供給中" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -22610,6 +26552,12 @@ "value" : "Posizione precisa" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "精密位置" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -22632,6 +26580,12 @@ "value" : "Preimpostazioni" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "プリセット" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -22660,6 +26614,12 @@ "value" : "Pin a pressione" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "プレスピン" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -22682,6 +26642,12 @@ "value" : "Pressione" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "気圧" + } + }, "zh-Hant-TW" : { "stringUnit" : { "state" : "translated", @@ -22716,6 +26682,12 @@ "value" : "Principale" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "プライマリ" + } + }, "pl" : { "stringUnit" : { "state" : "translated", @@ -22762,6 +26734,12 @@ "value" : "Chiave amministrativa primaria" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "プライマリ管理キー" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -22790,6 +26768,12 @@ "value" : "GPIO principale" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "プライマリGPIO" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -22818,6 +26802,12 @@ "value" : "Chiave privata" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "秘密キー" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -22864,6 +26854,12 @@ "value" : "Processo" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "プロセス" + } + }, "pl" : { "stringUnit" : { "state" : "translated", @@ -22904,6 +26900,12 @@ "value" : "Informazioni sul progetto" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "プロジェクト情報" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -22950,6 +26952,12 @@ "value" : "Protobufs" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "プロトコルバッファ" + } + }, "pl" : { "stringUnit" : { "state" : "translated", @@ -22996,6 +27004,12 @@ "value" : "Chiave pubblica" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "公開キー" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -23024,6 +27038,12 @@ "value" : "Crittografia a chiave pubblica" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "公開鍵暗号化" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -23052,6 +27072,12 @@ "value" : "Mancata corrispondenza della chiave pubblica" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "公開キー不一致" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -23080,6 +27106,12 @@ "value" : "Alimentato" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "パスワード" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -23120,6 +27152,12 @@ "value" : "Interrogativo" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "質問" + } + }, "pl" : { "stringUnit" : { "state" : "translated", @@ -23160,6 +27198,12 @@ "value" : "Radiazioni" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "放射線" + } + }, "zh-Hant-TW" : { "stringUnit" : { "state" : "translated", @@ -23194,6 +27238,12 @@ "value" : "Configurazione radio" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "無線設定" + } + }, "pl" : { "stringUnit" : { "state" : "translated", @@ -23234,6 +27284,12 @@ "value" : "Radio scollegata" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "無線切断" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -23274,6 +27330,12 @@ "value" : "Modulo encoder rotativo RAK" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "RAKロータリーエンコーダー" + } + }, "pl" : { "stringUnit" : { "state" : "translated", @@ -23332,6 +27394,12 @@ "value" : "Test di portata" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "レンジテスト" + } + }, "pl" : { "stringUnit" : { "state" : "translated", @@ -23390,6 +27458,12 @@ "value" : "Configurazione del test di portata" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "レンジテスト設定" + } + }, "pl" : { "stringUnit" : { "state" : "translated", @@ -23448,6 +27522,12 @@ "value" : "Configurazione del modulo Range Test ricevuta: %@" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "範囲テストモジュール設定を受信しました: %@" + } + }, "pl" : { "stringUnit" : { "state" : "translated", @@ -23506,6 +27586,12 @@ "value" : "Riavvio" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "再起動" + } + }, "pl" : { "stringUnit" : { "state" : "translated", @@ -23564,6 +27650,12 @@ "value" : "Riavviare il nodo?" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "ノードを再起動しますか?" + } + }, "pl" : { "stringUnit" : { "state" : "translated", @@ -23598,6 +27690,12 @@ }, "Rebroadcast any observed message, if it was on our private channel or from another mesh with the same lora params." : { "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "プライベートチャンネル上、または同じLoRaパラメータを持つ他のメッシュからの観測されたメッセージを再ブロードキャストします。" + } + }, "zh-Hant-TW" : { "stringUnit" : { "state" : "translated", @@ -23614,6 +27712,12 @@ "value" : "Modalità di ritrasmissione" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "再ブロードキャストモード" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -23642,6 +27746,12 @@ "value" : "Dati di ricezione (rxd) Pin GPIO" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "受信データ(RXD)GPIOピン" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -23682,6 +27792,12 @@ "value" : "Ricevuto un riscontro negativo" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "否定応答を受信しました" + } + }, "pl" : { "stringUnit" : { "state" : "translated", @@ -23740,6 +27856,12 @@ "value" : "Ricevuto Ack" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "受信確認" + } + }, "pl" : { "stringUnit" : { "state" : "translated", @@ -23798,6 +27920,12 @@ "value" : "Destinatario Ack" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "受信者確認" + } + }, "pl" : { "stringUnit" : { "state" : "translated", @@ -23844,6 +27972,12 @@ "value" : "Percorso di registrazione" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "ルート記録中" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -23866,6 +28000,12 @@ "value" : "Aggiornare i metadati del dispositivo" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "デバイスメタデータを更新" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -23881,7 +28021,14 @@ } }, "Regenerate Private Key" : { - + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "プライベートキーを再生成" + } + } + } }, "Region" : { "localizations" : { @@ -23897,6 +28044,12 @@ "value" : "Regione" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "地域" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -23943,6 +28096,12 @@ "value" : "Raggiunto il limite del ciclo di lavoro regionale" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "地域デューティサイクル制限に到達" + } + }, "pl" : { "stringUnit" : { "state" : "translated", @@ -23983,6 +28142,12 @@ "value" : "Note di rilascio" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "リリースノート" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -24005,6 +28170,12 @@ "value" : "Amministrazione remota per: %@" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "%@ のリモート管理" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -24027,6 +28198,12 @@ "value" : "Amministratore legacy remoto: %@" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "リモートレガシー管理: %@" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -24049,6 +28226,12 @@ "value" : "Amministratore PKI remoto: %@" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "リモートPKI管理: %@" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -24077,6 +28260,12 @@ "value" : "Elimina" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "削除" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -24105,6 +28294,12 @@ "value" : "Rimuovi dai preferiti" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "お気に入りから削除" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -24127,6 +28322,12 @@ "value" : "Elimina da ignorati" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "無視リストから削除" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -24161,6 +28362,12 @@ "value" : "Ripetitore" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "リピーター" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -24189,6 +28396,12 @@ "value" : "Sostituisci canali" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "チャンネル置換" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -24229,6 +28442,12 @@ "value" : "Risposta" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "返信" + } + }, "pl" : { "stringUnit" : { "state" : "translated", @@ -24365,6 +28584,12 @@ "value" : "Richiede la presenza di un accelerometro sul dispositivo." } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "デバイスに加速度計が必要です。" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -24455,6 +28680,12 @@ "value" : "Riavvio" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "再起動" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -24483,6 +28714,12 @@ "value" : "Riavviare al nodo a cui si è collegati" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "接続しているノードを再起動" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -24498,7 +28735,14 @@ } }, "Restore" : { - + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "復元" + } + } + } }, "Resume" : { "localizations" : { @@ -24526,6 +28770,12 @@ "value" : "Il curriculum" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "再開" + } + }, "pl" : { "stringUnit" : { "state" : "translated", @@ -24572,6 +28822,12 @@ "value" : "Esaminare l'applicazione" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "アプリをレビュー" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -24618,6 +28874,12 @@ "value" : "Diritto" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "右" + } + }, "pl" : { "stringUnit" : { "state" : "translated", @@ -24676,6 +28938,12 @@ "value" : "Suoneria" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "着信音" + } + }, "pl" : { "stringUnit" : { "state" : "translated", @@ -24728,6 +28996,12 @@ "value" : "Configurazione della suoneria" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "着信音設定" + } + }, "pl" : { "stringUnit" : { "state" : "translated", @@ -24768,6 +29042,12 @@ "value" : "Lingua di trasferimento della suoneria" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "着信音転送言語" + } + }, "se" : { "stringUnit" : { "state" : "translated", @@ -24854,6 +29134,12 @@ "value" : "Ruolo" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "役割" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -24910,6 +29196,12 @@ "value" : "Ruoli" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "役割" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -24932,6 +29224,12 @@ "value" : "Argomento radice" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "ルートトピック" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -24954,6 +29252,12 @@ "value" : "Rotary 1" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "ロータリー1" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -25076,6 +29380,12 @@ "value" : "Percorso: %@" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "ルート: %@" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -25104,6 +29414,12 @@ "value" : "Router" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "ルーター" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -25138,6 +29454,12 @@ "value" : "Router tardivo" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "ルーター遅延" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -25160,6 +29482,12 @@ "value" : "Percorsi" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "ルート" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -25240,6 +29568,12 @@ "value" : "RSSI %@ dBm" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "RSSI %@ dBm" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -25262,6 +29596,12 @@ "value" : "RSSI %ddB" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "RSSI %ddB" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -25284,6 +29624,12 @@ "value" : "RSSI %llddB" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "RSSI %llddB" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -25358,6 +29704,12 @@ }, "Russia" : { "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "ロシア" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -25380,6 +29732,12 @@ "value" : "Guadagno potenziato RX" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "RX ブーストゲイン" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -25418,6 +29776,12 @@ "value" : "לווין" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "衛星" + } + }, "pl" : { "stringUnit" : { "state" : "translated", @@ -25470,6 +29834,12 @@ "value" : "Sorvolo satellitare" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "衛星フライオーバー" + } + }, "pl" : { "stringUnit" : { "state" : "translated", @@ -25516,6 +29886,12 @@ "value" : "Sat" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "衛星" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -25544,6 +29920,12 @@ "value" : "Stima satelliti %lld" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "衛星推定数 %lld" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -25572,6 +29954,12 @@ "value" : "Satelliti in vista: %@" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "視野内衛星数: %@" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -25612,6 +30000,12 @@ "value" : "Salva" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "保存" + } + }, "pl" : { "stringUnit" : { "state" : "translated", @@ -25652,6 +30046,12 @@ "value" : "Salvare le impostazioni del canale" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "チャンネル設定を保存" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -25692,6 +30092,12 @@ "value" : "Salva la configurazione per %@" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "%@ の設定を保存" + } + }, "pl" : { "stringUnit" : { "state" : "translated", @@ -25738,6 +30144,12 @@ "value" : "Salvare la configurazione utente in %@?" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "ユーザー設定を %@ に保存しますか?" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -25766,6 +30178,12 @@ "value" : "Salva un CSV con i dettagli del messaggio di test di portata, attualmente disponibile solo sui dispositivi ESP32 con un server web." } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "レンジテストメッセージの詳細をCSVで保存します。現在、Webサーバーを持つESP32デバイスでのみ利用可能です。" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -25788,6 +30206,12 @@ }, "Scan this QR code to add %@ to another device." : { "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "このQRコードをスキャンして、%@ を別のデバイスに追加してください。" + } + }, "zh-Hant-TW" : { "stringUnit" : { "state" : "translated", @@ -25804,6 +30228,12 @@ "value" : "Schermo acceso per" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "画面オン時間" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -25832,6 +30262,12 @@ "value" : "Ricerca" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "検索" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -25854,6 +30290,12 @@ "value" : "Secondo" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "秒" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -25894,6 +30336,12 @@ "value" : "Secondario" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "副" + } + }, "pl" : { "stringUnit" : { "state" : "translated", @@ -25940,6 +30388,12 @@ "value" : "Chiave amministrativa secondaria" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "副管理者キー" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -25974,6 +30428,12 @@ "value" : "Sicurezza" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "セキュリティ" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -26008,6 +30468,12 @@ "value" : "Configurazione della sicurezza" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "セキュリティ設定" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -26042,6 +30508,12 @@ "value" : "Le impostazioni di configurazione della sicurezza richiedono una versione del firmware 2.5+" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "セキュリティ設定にはファームウェアバージョン2.5以上が必要です" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -26088,6 +30560,12 @@ "value" : "Selezionare" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "選択" + } + }, "pl" : { "stringUnit" : { "state" : "translated", @@ -26134,6 +30612,12 @@ "value" : "Selezionare un canale" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "チャンネルを選択" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -26156,6 +30640,12 @@ "value" : "Selezionare una conversazione" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "会話を選択" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -26178,6 +30668,12 @@ "value" : "Selezionare un tipo di conversazione" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "会話タイプを選択" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -26194,6 +30690,12 @@ }, "Select a node from the drop down to manage connected or remote devices." : { "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "ドロップダウンからノードを選択して、接続済みまたはリモートデバイスを管理してください。" + } + }, "zh-Hant-TW" : { "stringUnit" : { "state" : "translated", @@ -26210,6 +30712,12 @@ "value" : "Selezionare un percorso di tracciamento" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "トレースルートを選択" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -26232,6 +30740,12 @@ "value" : "Selezionare il canale" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "チャンネル選択" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -26272,6 +30786,12 @@ "value" : "Selezionare un nodo" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "ノード選択" + } + }, "pl" : { "stringUnit" : { "state" : "translated", @@ -26318,6 +30838,12 @@ "value" : "Inviare" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "送信" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -26390,6 +30916,12 @@ "value" : "Inviare un messaggio diretto" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "ダイレクトメッセージを送信" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -26418,6 +30950,12 @@ "value" : "Inviare un messaggio di gruppo" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "グループメッセージを送信" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -26534,6 +31072,12 @@ "value" : "Inviare uno spegnimento al nodo a cui si è connessi" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "接続しているノードにシャットダウン信号を送信" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -26618,6 +31162,12 @@ "value" : "Invia la campana" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "ベル送信" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -26664,6 +31214,12 @@ "value" : "Inviare il battito cardiaco" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "ハートビート送信" + } + }, "pl" : { "stringUnit" : { "state" : "translated", @@ -26704,6 +31260,12 @@ "value" : "Inviare il riavvio OTA" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "OTA再起動送信" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -26732,6 +31294,12 @@ "value" : "Intervallo del mittente" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "送信者間隔" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -26766,6 +31334,12 @@ "value" : "Sensore" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "センサー" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -26794,6 +31368,12 @@ "value" : "Opzioni del sensore" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "センサーオプション" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -26816,6 +31396,12 @@ "value" : "Opzioni del sensore" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "センサーオプション" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -26850,6 +31436,12 @@ "value" : "Inviato un canale per: %@ Canale Indice %d" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "%@ のチャンネルを送信しました(チャンネルインデックス %d)" + } + }, "pl" : { "stringUnit" : { "state" : "translated", @@ -27187,6 +31779,12 @@ "value" : "Numero di sequenza" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "シーケンス番号" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -27249,6 +31847,12 @@ "value" : "Seriale" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "シリアル" + } + }, "pl" : { "stringUnit" : { "state" : "translated", @@ -27307,6 +31911,12 @@ "value" : "Configurazione seriale" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "シリアル設定" + } + }, "pl" : { "stringUnit" : { "state" : "translated", @@ -27353,6 +31963,12 @@ "value" : "Console seriale" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "シリアルコンソール" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -27421,6 +32037,12 @@ "value" : "Configurazione modulo seriale ricevuta: %@" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "シリアルモジュール設定を受信しました: %@" + } + }, "pl" : { "stringUnit" : { "state" : "translated", @@ -27461,6 +32083,12 @@ "value" : "Serie" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "系列" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -27489,6 +32117,12 @@ "value" : "Server" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "サーバー" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -27517,6 +32151,12 @@ "value" : "Indirizzo del server" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "サーバーアドレス" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -27539,6 +32179,12 @@ "value" : "Opzione server" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "サーバーオプション" + } + }, "zh-Hant-TW" : { "stringUnit" : { "state" : "translated", @@ -27555,6 +32201,12 @@ "value" : "Set" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "設定" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -27595,6 +32247,12 @@ "value" : "Impostare la regione LoRa" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "LoRa地域を設定" + } + }, "pl" : { "stringUnit" : { "state" : "translated", @@ -27635,6 +32293,12 @@ "value" : "Impostare i pin GPIO per RXD e TXD." } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "RXDとTXDのGPIOピンを設定します。" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -27650,7 +32314,14 @@ } }, "Set to current location" : { - + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "現在の位置に設定" + } + } + } }, "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" : { @@ -27675,7 +32346,14 @@ } }, "Sets the screen clock format to 12-hour." : { - + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "画面の時計表示を12時間形式に設定します。" + } + } + } }, "Settings" : { "localizations" : { @@ -27703,6 +32381,12 @@ "value" : "Impostazioni" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "設定" + } + }, "pl" : { "stringUnit" : { "state" : "translated", @@ -27761,6 +32445,12 @@ "value" : "Settantadue ore" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "72時間" + } + }, "pl" : { "stringUnit" : { "state" : "translated", @@ -27795,6 +32485,12 @@ }, "Share Contact QR" : { "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "連絡先QRを共有" + } + }, "zh-Hant-TW" : { "stringUnit" : { "state" : "translated", @@ -27829,6 +32525,12 @@ "value" : "Condividi il codice QR" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "QRコードを共有" + } + }, "pl" : { "stringUnit" : { "state" : "translated", @@ -27875,6 +32577,12 @@ "value" : "Condividi il codice QR e il link" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "QRコードとリンクを共有" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -27903,6 +32611,12 @@ "value" : "Chiave condivisa" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "共有キー" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -27943,6 +32657,12 @@ "value" : "Condividere i canali Meshtastic" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "Meshtasticチャンネル共有" + } + }, "pl" : { "stringUnit" : { "state" : "translated", @@ -27989,6 +32709,12 @@ "value" : "Nome breve" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "短い名前" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -28017,6 +32743,12 @@ "value" : "Corto raggio - Veloce" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "短距離 - 高速" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -28039,6 +32771,12 @@ "value" : "Corto raggio - Lento" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "短距離 - 低速" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -28061,6 +32799,12 @@ "value" : "Corto raggio - Turbo" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "短距離 - ターボ" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -28089,6 +32833,12 @@ "value" : "Mostra avvisi" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "アラート表示" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -28117,6 +32867,12 @@ "value" : "Mostra avvisi" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "アラート表示" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -28145,6 +32901,12 @@ "value" : "Mostra i nodi" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "ノード表示" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -28173,6 +32935,12 @@ "value" : "Mostra sullo schermo del dispositivo" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "デバイス画面に表示" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -28229,6 +32997,12 @@ "value" : "Mostra waypoint " } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "ウェイポイントを表示" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -28251,6 +33025,12 @@ "value" : "Mostra le informazioni relative alla radio Lora collegata via bluetooth. È possibile scorrere il dito verso sinistra per scollegare la radio e premere a lungo per avviare l'attività live." } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "Bluetoothで接続されたLoRaラジオの情報を表示します。左にスワイプしてラジオを切断し、長押しでライブアクティビティを開始できます。" + } + }, "zh-Hant-TW" : { "stringUnit" : { "state" : "translated", @@ -28273,6 +33053,12 @@ "value" : "Spegnimento" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "シャットダウン" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -28301,6 +33087,12 @@ "value" : "Spegnere il nodo?" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "ノードをシャットダウンしますか?" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -28329,6 +33121,12 @@ "value" : "Arresto del nodo?" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "ノードをシャットダウンしますか?" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -28363,6 +33161,12 @@ "value" : "Spegnimento in caso di perdita di alimentazione" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "電源喪失時にシャットダウン" + } + }, "pl" : { "stringUnit" : { "state" : "translated", @@ -28403,6 +33207,12 @@ "value" : "Segnale %@" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "信号 %@" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -28443,6 +33253,12 @@ "value" : "Semplice" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "シンプル" + } + }, "pl" : { "stringUnit" : { "state" : "translated", @@ -28483,6 +33299,12 @@ "value" : "Singapore 923MHz" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "シンガポール 923MHz" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -28523,6 +33345,12 @@ "value" : "Sei ore" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "6時間" + } + }, "pl" : { "stringUnit" : { "state" : "translated", @@ -28569,6 +33397,12 @@ "value" : "Sci" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "スキー" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -28597,6 +33431,12 @@ "value" : "Posizione intelligente" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "スマート位置" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -28619,6 +33459,12 @@ "value" : "SNR" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "SNR" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -28641,6 +33487,12 @@ "value" : "SNR %@ dB" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "SNR %@ dB" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -28663,6 +33515,12 @@ "value" : "SNR %@dB" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "SNR %@dB" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -28685,6 +33543,12 @@ "value" : "Umidità del suolo" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "土壌水分" + } + }, "zh-Hant-TW" : { "stringUnit" : { "state" : "translated", @@ -28701,6 +33565,12 @@ "value" : "Temperatura del suolo" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "土壌温度" + } + }, "zh-Hant-TW" : { "stringUnit" : { "state" : "translated", @@ -28717,6 +33587,12 @@ "value" : "Specifica la durata dell'uscita del GPIO monitorato." } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "監視対象GPIOの出力時間を指定。" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -28745,6 +33621,12 @@ "value" : "Velocità" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "速度" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -28773,6 +33655,12 @@ "value" : "Velocità %@" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "速度 %@" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -28801,6 +33689,12 @@ "value" : "Velocità: %@" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "速度: %@" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -28816,7 +33710,14 @@ } }, "Sponsor App Development" : { - + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "アプリ開発をスポンサー" + } + } + } }, "Spread Factor" : { "localizations" : { @@ -28826,6 +33727,12 @@ "value" : "Fattore di diffusione" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "拡散係数" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -28866,6 +33773,12 @@ "value" : "SSID" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "SSID" + } + }, "pl" : { "stringUnit" : { "state" : "translated", @@ -28912,6 +33825,12 @@ "value" : "Predefinito" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "標準" + } + }, "pl" : { "stringUnit" : { "state" : "translated", @@ -28958,6 +33877,12 @@ "value" : "Predefinito Silenzioso" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "標準ミュート" + } + }, "pl" : { "stringUnit" : { "state" : "translated", @@ -29016,6 +33941,12 @@ "value" : "Inizio" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "開始" + } + }, "pl" : { "stringUnit" : { "state" : "translated", @@ -29056,6 +33987,12 @@ "value" : "Stato Intervallo di trasmissione" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "状態ブロードキャスト間隔" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -29208,6 +34145,12 @@ "value" : "Abbonati" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "購読済み" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -29230,6 +34173,12 @@ "value" : "Sottosistema" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "サブシステム" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -29258,6 +34207,12 @@ "value" : "Supportato" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "サポート済み" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -29308,6 +34263,12 @@ "value" : "Tabella" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "テーブル" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -29324,6 +34285,12 @@ }, "Taiwan" : { "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "台湾" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -29352,6 +34319,12 @@ "value" : "TAK" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "TAK" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -29386,6 +34359,12 @@ "value" : "Tracker TAK" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "TAKトラッカー" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -29414,6 +34393,12 @@ "value" : "Prende l'URL di un canale Meshtastic e salva le impostazioni del canale." } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "MeshtasticチャンネルURLを取得し、チャンネル設定を保存します。" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -29430,6 +34415,12 @@ }, "Takes a Meshtastic contact URL and saves it to the nodes database" : { "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "Meshtastic連絡先URLを取得し、ノードデータベースに保存します" + } + }, "zh-Hant-TW" : { "stringUnit" : { "state" : "translated", @@ -29464,6 +34455,12 @@ "value" : "Risposta di Tapback" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "タップバック" + } + }, "pl" : { "stringUnit" : { "state" : "translated", @@ -29522,6 +34519,12 @@ "value" : "Telemetria (sensori)" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "テレメトリ" + } + }, "pl" : { "stringUnit" : { "state" : "translated", @@ -29580,6 +34583,12 @@ "value" : "Configurazione della telemetria" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "テレメトリ設定" + } + }, "pl" : { "stringUnit" : { "state" : "translated", @@ -29742,6 +34751,12 @@ "value" : "Temp" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "温度" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -29770,6 +34785,12 @@ "value" : "Temperatura" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "温度" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -29810,6 +34831,12 @@ "value" : "Dieci minuti" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "10分" + } + }, "pl" : { "stringUnit" : { "state" : "translated", @@ -29868,6 +34895,12 @@ "value" : "Dieci secondi" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "10秒" + } + }, "pl" : { "stringUnit" : { "state" : "translated", @@ -29914,6 +34947,12 @@ "value" : "Chiave amministrativa terziaria" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "第三管理者キー" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -29960,6 +34999,12 @@ "value" : "Messaggio di testo" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "テキストメッセージ" + } + }, "pl" : { "stringUnit" : { "state" : "translated", @@ -30000,6 +35045,12 @@ "value" : "Display TFT a colori" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "TFTフルカラーディスプレイ" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -30022,6 +35073,12 @@ "value" : "Thailandia" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "タイ" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -30044,6 +35101,12 @@ "value" : "Il tempo di attesa prima che il pacchetto venga considerato completato." } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "パケットが完了したと見なすまでの待機時間。" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -30066,6 +35129,12 @@ "value" : "La direzione della bussola sullo schermo all'esterno del cerchio punterà sempre verso nord." } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "画面上の円の外側にあるコンパスの方位は常に北を指します。" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -30094,6 +35163,12 @@ "value" : "Il punto di rugiada è %@ in questo momento." } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "現在の露点は %@ です。" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -30116,6 +35191,12 @@ "value" : "La velocità con cui verranno inviati gli aggiornamenti della posizione se la distanza minima è stata soddisfatta" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "最小距離条件が満たされた場合の位置更新送信の最短間隔" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -30138,6 +35219,12 @@ "value" : "Il formato utilizzato per visualizzare le coordinate GPS sullo schermo del dispositivo." } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "デバイス画面でGPS座標を表示する形式。" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -30166,6 +35253,12 @@ "value" : "Gli ultimi 4 dell'indirizzo MAC del dispositivo vengono aggiunti al nome breve per impostare il nome BLE del dispositivo. Il nome breve può avere una lunghezza massima di 4 byte." } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "デバイスのBLE名を設定するため、MACアドレスの末尾4桁が短縮名に追加されます。短縮名は最大4バイトまでです。" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -30194,6 +35287,12 @@ "value" : "L'intervallo massimo che può trascorrere senza che un nodo trasmetta una posizione" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "ノードが位置をブロードキャストしない最大間隔" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -30216,6 +35315,12 @@ "value" : "Le applicazioni Meshtastic Apple supportano la versione firmware %@ e successive." } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "Meshtastic Appleアプリはファームウェアバージョン %@ 以上をサポートしています。" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -30244,6 +35349,12 @@ "value" : "La variazione di distanza minima in metri da considerare per la trasmissione di una posizione intelligente." } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "スマート位置ブロードキャストで考慮される最小距離変化(メートル)。" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -30318,6 +35429,12 @@ "value" : "Il pacchetto è troppo grande" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "パケットが大きすぎます" + } + }, "pl" : { "stringUnit" : { "state" : "translated", @@ -30364,6 +35481,12 @@ "value" : "La chiave pubblica primaria autorizzata a inviare messaggi di amministrazione a questo nodo." } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "このノードに管理メッセージを送信する権限を持つプライマリ公開キー。" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -30420,6 +35543,12 @@ "value" : "La regione in cui si utilizzeranno le radio." } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "無線機を使用する地域。" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -30448,6 +35577,12 @@ "value" : "L'argomento principale da usare per MQTT." } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "MQTTに使用するルートトピック。" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -30498,6 +35633,12 @@ "value" : "La chiave pubblica secondaria autorizzata a inviare messaggi di amministrazione a questo nodo." } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "このノードに管理メッセージを送信する権限を持つセカンダリ公開キー。" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -30526,6 +35667,12 @@ "value" : "Il dispositivo specificato si è disconnesso" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "指定されたデバイスが切断されました" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -30548,6 +35695,12 @@ "value" : "Lo stato del LED (acceso/spento)" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "LEDの状態(オン/オフ)" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -30698,6 +35851,12 @@ "value" : "Trenta minuti" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "30分" + } + }, "pl" : { "stringUnit" : { "state" : "translated", @@ -30756,6 +35915,12 @@ "value" : "Trenta secondi" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "30秒" + } + }, "pl" : { "stringUnit" : { "state" : "translated", @@ -30814,6 +35979,12 @@ "value" : "Trentasei ore" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "36時間" + } + }, "pl" : { "stringUnit" : { "state" : "translated", @@ -30854,6 +36025,12 @@ "value" : "Questa conversazione sarà cancellata." } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "この会話は削除されます。" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -30932,6 +36109,12 @@ "value" : "È probabile che questo messaggio non sia stato consegnato." } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "このメッセージは配信されなかった可能性があります。" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -30954,6 +36137,12 @@ "value" : "Questo nodo non supporta alcun modulo configurabile." } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "このノードは設定可能なモジュールをサポートしていません。" + } + }, "zh-Hant-TW" : { "stringUnit" : { "state" : "translated", @@ -31038,6 +36227,12 @@ "value" : "Tre ore" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "3時間" + } + }, "pl" : { "stringUnit" : { "state" : "translated", @@ -31096,6 +36291,12 @@ "value" : "Tre secondi" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "3秒" + } + }, "pl" : { "stringUnit" : { "state" : "translated", @@ -31258,6 +36459,12 @@ "value" : "Tempo" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "時刻" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -31286,6 +36493,12 @@ "value" : "Timbro del tempo" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "タイムスタンプ" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -31314,6 +36527,12 @@ "value" : "Fuso orario" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "タイムゾーン" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -31382,6 +36601,12 @@ "value" : "Timeout" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "タイムアウト" + } + }, "pl" : { "stringUnit" : { "state" : "translated", @@ -31440,6 +36665,12 @@ "value" : "Timestamp" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "タイムスタンプ" + } + }, "pl" : { "stringUnit" : { "state" : "translated", @@ -31480,6 +36711,12 @@ "value" : "Tempi e formati" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "タイミング・フォーマット" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -31502,6 +36739,12 @@ "value" : "TLS abilitato" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "TLS有効" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -31568,6 +36811,12 @@ "value" : "Totale" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "合計" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -31590,6 +36839,12 @@ "value" : "Totale PAX" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "総PAX" + } + }, "se" : { "stringUnit" : { "state" : "translated", @@ -31618,6 +36873,12 @@ "value" : "Percorso di tracciamento" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "トレースルート" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -31650,6 +36911,12 @@ "value" : "Registro del percorso di tracciamento" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "トレースルートログ" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -31730,6 +36997,12 @@ "value" : "Traccia del percorso inviato" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "トレースルート送信済み" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -31812,6 +37085,12 @@ }, "Tracker" : { "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "トラッカー" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -31846,6 +37125,12 @@ "value" : "Traffico" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "トラフィック" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -31868,6 +37153,12 @@ "value" : "Dati di trasmissione (txd) Pin GPIO" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "送信データ(TXD)GPIOピン" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -31890,6 +37181,12 @@ "value" : "Trasmissione abilitata" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "送信有効" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -31918,6 +37215,12 @@ "value" : "Tratta il doppio tocco sugli accelerometri supportati come una pressione di un tasto utente." } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "サポートされている加速度計でのダブルタップをユーザーボタン押下として扱います。" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -31946,6 +37249,12 @@ "value" : "Tipo di innesco" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "トリガータイプ" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -31968,6 +37277,12 @@ "value" : "Ping ad hoc a triplo clic" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "トリプルクリック アドホックPing" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -31996,6 +37311,12 @@ "value" : "Riprova" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "再試行" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -32036,6 +37357,12 @@ "value" : "Dodici ore" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "12時間" + } + }, "pl" : { "stringUnit" : { "state" : "translated", @@ -32094,6 +37421,12 @@ "value" : "Ventiquattro ore" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "24時間" + } + }, "pl" : { "stringUnit" : { "state" : "translated", @@ -32152,6 +37485,12 @@ "value" : "Due ore" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "2時間" + } + }, "pl" : { "stringUnit" : { "state" : "translated", @@ -32210,6 +37549,12 @@ "value" : "Due minuti" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "2分" + } + }, "pl" : { "stringUnit" : { "state" : "translated", @@ -32268,6 +37613,12 @@ "value" : "Due secondi" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "2秒" + } + }, "pl" : { "stringUnit" : { "state" : "translated", @@ -32308,6 +37659,12 @@ "value" : "Trasmissione UDP" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "UDPブロードキャスト" + } + }, "zh-Hant-TW" : { "stringUnit" : { "state" : "translated", @@ -32324,6 +37681,12 @@ "value" : "Ucraina 433MHz" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "ウクライナ 433MHz" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -32346,6 +37709,12 @@ "value" : "Ucraina 868MHz" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "ウクライナ 868MHz" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -32368,6 +37737,12 @@ "value" : "Non preferito" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "お気に入りを解除" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -32390,6 +37765,12 @@ "value" : "Non sano" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "不健康" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -32412,6 +37793,12 @@ "value" : "Insalubre per i gruppi sensibili" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "敏感なグループには不健康" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -32434,6 +37821,12 @@ "value" : "Stati Uniti" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "アメリカ合衆国" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -32456,6 +37849,12 @@ "value" : "Unità visualizzate sullo schermo del dispositivo" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "デバイス画面に表示される単位" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -32496,6 +37895,12 @@ "value" : "Mercatore Universale Trasverso" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "ユニバーサル横メルカトル図法" + } + }, "pl" : { "stringUnit" : { "state" : "translated", @@ -32536,6 +37941,12 @@ "value" : "sconosciuto" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "不明" + } + }, "sr" : { "stringUnit" : { "state" : "needs_review", @@ -32570,6 +37981,12 @@ "value" : "Sconosciuto" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "不明" + } + }, "pl" : { "stringUnit" : { "state" : "translated", @@ -32628,6 +38045,12 @@ "value" : "Età sconosciuta" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "不明な経過時間" + } + }, "pl" : { "stringUnit" : { "state" : "translated", @@ -32661,10 +38084,24 @@ } }, "Unmessagable" : { - + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "メッセージ不可" + } + } + } }, "Unmonitored" : { - + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "監視なし" + } + } + } }, "Unset" : { "localizations" : { @@ -32692,6 +38129,12 @@ "value" : "Non impostato" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "未設定" + } + }, "pl" : { "stringUnit" : { "state" : "translated", @@ -32732,6 +38175,12 @@ "value" : "Non supportato" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "サポート対象外" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -32772,6 +38221,12 @@ "value" : "Rilevata versione firmware non supportata, impossibile connettersi al dispositivo." } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "サポートされていないファームウェアバージョンが検出されました。デバイスに接続できません。" + } + }, "pl" : { "stringUnit" : { "state" : "translated", @@ -32830,6 +38285,12 @@ "value" : "Su" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "上" + } + }, "pl" : { "stringUnit" : { "state" : "translated", @@ -32870,6 +38331,12 @@ "value" : "Su Giù 1" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "上下 1" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -32916,6 +38383,12 @@ "value" : "fino a %@ di distanza" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "最大 %@ 離れています" + } + }, "pl" : { "stringUnit" : { "state" : "translated", @@ -32956,6 +38429,12 @@ "value" : "Intervallo di aggiornamento" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "更新間隔" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -32996,6 +38475,12 @@ "value" : "Aggiornare il firmware" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "ファームウェアを更新" + } + }, "pl" : { "stringUnit" : { "state" : "translated", @@ -33036,6 +38521,12 @@ "value" : "Dati aggiornati sulle statistiche dei nodi." } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "ノード統計データを更新しました。" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -33064,6 +38555,12 @@ "value" : "Aggiornato: %@" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "更新日時: %@" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -33086,6 +38583,12 @@ "value" : "Uplink abilitato" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "アップリンク有効" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -33114,6 +38617,12 @@ "value" : "Tempo di attività" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "稼働時間" + } + }, "se" : { "stringUnit" : { "state" : "translated", @@ -33170,6 +38679,12 @@ "value" : "Utilizzare I2S come cicalino" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "I2Sをブザーとして使用" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -33185,7 +38700,14 @@ } }, "Use my Location" : { - + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "自分の位置を使用" + } + } + } }, "Use Preset" : { "localizations" : { @@ -33195,6 +38717,12 @@ "value" : "Utilizzare la preimpostazione" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "プリセットを使用" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -33223,6 +38751,12 @@ "value" : "Utilizzare il cicalino PWM" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "PWMブザーを使用" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -33266,7 +38800,14 @@ } }, "Used to identify unmonitored or infrastructure nodes so that messaging is not avaliable to nodes that will never respond." : { - + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "監視されていないまたはインフラストラクチャノードを識別するために使用され、応答しないノードにはメッセージング機能が利用できないようにします。" + } + } + } }, "User" : { "localizations" : { @@ -33294,6 +38835,12 @@ "value" : "Utente" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "ユーザー" + } + }, "pl" : { "stringUnit" : { "state" : "translated", @@ -33334,6 +38881,12 @@ "value" : "Configurazione utente" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "ユーザー設定" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -33362,6 +38915,12 @@ "value" : "Dettagli utente" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "ユーザー詳細" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -33390,6 +38949,12 @@ "value" : "Id utente" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "ユーザーID" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -33418,6 +38983,12 @@ "value" : "Disconnessione avviata dall'utente" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "ユーザーによる切断" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -33464,6 +39035,12 @@ "value" : "Nome utente" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "ユーザー名" + } + }, "pl" : { "stringUnit" : { "state" : "translated", @@ -33504,6 +39081,12 @@ "value" : "Utilizza una resistenza di pullup" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "プルアップ抵抗を使用" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -33526,6 +39109,12 @@ "value" : "Utilizza la connessione di rete del telefono per connettersi a MQTT." } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "スマートフォンのネットワーク接続を利用してMQTTに接続します。" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -33560,6 +39149,12 @@ "value" : "Direzione del veicolo" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "車両方位" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -33588,6 +39183,12 @@ "value" : "Velocità del veicolo" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "車両速度" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -33644,6 +39245,12 @@ "value" : "Version: %1$@ (%2$@)" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "バージョン: %@ (%@)" + } + }, "zh-Hant-TW" : { "stringUnit" : { "state" : "translated", @@ -33666,6 +39273,12 @@ "value" : "Versione: %1$@ (%2$@) " } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "バージョン: %1$@ (%2$@)" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -33694,6 +39307,12 @@ "value" : "Molto malsano" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "非常に不健康" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -33722,6 +39341,12 @@ "value" : "Via Lora" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "LoRa経由" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -33750,6 +39375,12 @@ "value" : "Via Mqtt" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "MQTT経由" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -33796,6 +39427,12 @@ "value" : "Tensione" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "電圧" + } + }, "pl" : { "stringUnit" : { "state" : "translated", @@ -33836,6 +39473,12 @@ "value" : "Volt %@" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "Volts %@" + } + }, "zh-Hant-TW" : { "stringUnit" : { "state" : "translated", @@ -33870,6 +39513,12 @@ "value" : "In attesa. . ." } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "待機中" + } + }, "pl" : { "stringUnit" : { "state" : "translated", @@ -33960,6 +39609,12 @@ "value" : "Camminare" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "歩行" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -33994,6 +39649,12 @@ "value" : "Onda" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "波" + } + }, "se" : { "stringUnit" : { "state" : "translated", @@ -34015,7 +39676,14 @@ } }, "Waypoint Failed to Send" : { - + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "ウェイポイントの送信に失敗" + } + } + } }, "Waypoint Options" : { "localizations" : { @@ -34031,6 +39699,12 @@ "value" : "Opzioni Waypoint" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "ウェイポイントオプション" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -34117,6 +39791,12 @@ "value" : "Condizioni meteo" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "気象条件" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -34145,6 +39825,12 @@ "value" : "Lampeggiatore web" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "ウェブフラッシャー" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -34167,6 +39853,12 @@ "value" : "Sito web" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "ウェブサイト" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -34195,6 +39887,12 @@ "value" : "Peso" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "重量" + } + }, "zh-Hant-TW" : { "stringUnit" : { "state" : "translated", @@ -34217,6 +39915,12 @@ "value" : "Che cosa significa il lucchetto?" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "鍵マークの意味は?" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -34251,6 +39955,12 @@ "value" : "Che cos'è Meshtastic?" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "Meshtasticとは?" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -34279,6 +39989,12 @@ "value" : "Cosa fa la modalità operatore con licenza:\n* Imposta il nome del nodo con il proprio nominativo\n* Trasmette informazioni sul nodo ogni 10 minuti\n* Sovrascrive la frequenza, il dutycycle e la potenza di trasmissione\n* Disabilita la crittografia" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "ライセンス操作者モードの機能:\n* ノード名をコールサインに設定\n* 10分ごとにノード情報をブロードキャスト\n* 周波数、デューティサイクル、送信電力をオーバーライド\n* 暗号化を無効化" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -34347,6 +40063,12 @@ "value" : "Quando si utilizza la modalità GPIO, mantenere l'uscita attiva per questo tempo. " } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "GPIOモードで使用する際、この期間出力をオンに保ちます。" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -34375,6 +40097,12 @@ "value" : "Utilizza o meno la modalità INPUT_PULLUP per il pin GPIO. Si applica solo se la scheda utilizza resistenze di pull-up sul pin" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "GPIOピンでINPUT_PULLUPモードを使用するかどうか。ボードがピンでプルアップ抵抗を使用している場合のみ適用されます" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -34397,6 +40125,12 @@ "value" : "WiFi" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "WiFi" + } + }, "se" : { "stringUnit" : { "state" : "translated", @@ -34431,6 +40165,12 @@ "value" : "Opzioni WiFi" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "WiFiオプション" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -34499,6 +40239,12 @@ "value" : "Vento" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "風" + } + }, "zh-Hant-TW" : { "stringUnit" : { "state" : "translated", @@ -34521,6 +40267,12 @@ "value" : "Direzione del vento" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "風向" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -34549,6 +40301,12 @@ "value" : "Velocità del vento" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "風速" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -34577,6 +40335,12 @@ "value" : "Entro il %@" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "%@ 以内" + } + }, "se" : { "stringUnit" : { "state" : "translated", @@ -34605,6 +40369,12 @@ "value" : "x" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "x" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -34633,6 +40403,12 @@ "value" : "X: %1$@, Y: %2$d" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "X: %1$@, Y: %2$d" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -34668,6 +40444,12 @@ "value" : "X: %1$@, Y: %2$f" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "X: %1$@, Y: %2$f" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -34703,6 +40485,12 @@ "value" : "X: %1$@, Y: %2$lld" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "X: %1$@, Y: %2$lld" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -34732,6 +40520,12 @@ "value" : "y" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "y" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -34766,6 +40560,12 @@ "value" : "Ieri" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "昨日" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -34788,6 +40588,12 @@ "value" : "È anche possibile aggiornare il dispositivo Meshtastic tramite bluetooth utilizzando l'applicazione Nordic DFU." } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "Nordic DFUアプリを使用してBluetoothでMeshtasticデバイスを更新することもできます。" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -34834,6 +40640,12 @@ "value" : "È possibile inviare e ricevere messaggi di canale (chat di gruppo) e messaggi diretti. Da qualsiasi messaggio è possibile premere a lungo per visualizzare le azioni disponibili, come copia, risposta, tapback e cancellazione, nonché i dettagli di consegna." } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "チャンネル(グループチャット)とダイレクトメッセージの送受信ができます。任意のメッセージを長押しすると、コピー、返信、タップバック、削除などの利用可能なアクションと配信詳細を表示できます。" + } + }, "pl" : { "stringUnit" : { "state" : "translated", @@ -34874,6 +40686,12 @@ "value" : "La posizione attuale viene impostata come posizione fissa e trasmessa sulla mesh nell'intervallo di posizione." } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "現在の位置が固定位置として設定され、位置間隔でメッシュネットワーク上にブロードキャストされます。" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -34908,6 +40726,12 @@ "value" : "Il firmware è aggiornato" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "ファームウェアは最新です" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -34936,6 +40760,12 @@ "value" : "Il server MQTT deve supportare TLS." } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "MQTTサーバーはTLSをサポートする必要があります。" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -34952,6 +40782,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" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "ノードは設定されたMQTTサーバーに定期的に暗号化されていないマップレポートパケットを送信します。これにはID、短縮名と長い名前、おおよその位置、ハードウェアモデル、役割、ファームウェアバージョン、LoRa地域、モデムプリセット、プライマリチャンネル名が含まれます。" + } + }, "zh-Hant-TW" : { "stringUnit" : { "state" : "translated", @@ -34968,6 +40804,12 @@ "value" : "La frequenza operativa del nodo viene calcolata in base alla regione, alla preimpostazione del modem e a questo campo. Se il campo è 0, lo slot viene calcolato automaticamente in base al nome del canale principale." } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "ノードの動作周波数は、地域、モデムプリセット、およびこのフィールドに基づいて計算されます。0の場合、スロットはプライマリチャンネル名に基づいて自動的に計算されます。" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -34990,6 +40832,12 @@ "value" : "La vostra posizione è stata inviata con una richiesta di risposta con la loro posizione. Riceverete una notifica quando la posizione verrà restituita." } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "位置情報が位置の返信要求と共に送信されました。位置が返信されると通知を受け取ります。" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -35018,6 +40866,12 @@ "value" : "La vostra regione ha un ciclo di lavoro di %lld%%. MQTT è sconsigliato quando il ciclo di lavoro è limitato, perché il traffico extra sovraccaricherà rapidamente la rete LoRa." } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "お住まいの地域はデューティサイクルが%lld%%です。デューティサイクル制限がある場合、MQTTの使用は推奨されません。追加のトラフィックによってLoRaメッシュがすぐに圧迫されます。" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -35046,6 +40900,12 @@ "value" : "La regione ha un ciclo di funzionamento orario del %lld%%; la radio smette di inviare pacchetti quando raggiunge il limite orario." } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "お住まいの地域は時間あたり%lld%%のデューティサイクル制限があります。無線機が時間制限に達すると、パケットの送信を停止します。" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -35068,6 +40928,12 @@ "value" : "Il file di percorso deve avere entrambe le colonne Latitudine e Longitudine e le intestazioni." } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "ルートファイルには緯度と経度の列とヘッダーの両方が必要です。" + } + }, "sr" : { "stringUnit" : { "state" : "translated", From fc206bbc05e1d609a939d50939f54ea9bb6cf03a Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Mon, 23 Jun 2025 09:25:39 -0700 Subject: [PATCH 171/213] Reboot on key changes --- Meshtastic/Views/Settings/Config/SecurityConfig.swift | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/Meshtastic/Views/Settings/Config/SecurityConfig.swift b/Meshtastic/Views/Settings/Config/SecurityConfig.swift index e252819a..1897bf43 100644 --- a/Meshtastic/Views/Settings/Config/SecurityConfig.swift +++ b/Meshtastic/Views/Settings/Config/SecurityConfig.swift @@ -349,6 +349,14 @@ struct SecurityConfig: View { } } hasChanges = false + if keyUpdated { + if !bleManager.sendReboot( + fromUser: fromUser, + toUser: toUser + ) { + Logger.mesh.warning("Reboot Failed") + } + } goBack() } } From 90a8a4efa83fb5a83f81fd9463046f8f45de99f9 Mon Sep 17 00:00:00 2001 From: kanakonagiri Date: Tue, 24 Jun 2025 15:00:26 +0900 Subject: [PATCH 172/213] =?UTF-8?q?add:=20=E6=97=A5=E6=9C=AC=E8=AA=9E?= =?UTF-8?q?=E8=A8=B3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Localizable.xcstrings | 252 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 252 insertions(+) diff --git a/Localizable.xcstrings b/Localizable.xcstrings index 124ce540..f8d14a08 100644 --- a/Localizable.xcstrings +++ b/Localizable.xcstrings @@ -2639,6 +2639,12 @@ "value" : "Aggiungi nodo Meshtastic %@ ai contatti" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "Meshtasticノード%@を連絡先に追加" + } + }, "zh-Hant-TW" : { "stringUnit" : { "state" : "translated", @@ -3539,6 +3545,12 @@ "value" : "Altitudine %@" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "高度 %@" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -4397,6 +4409,12 @@ "value" : "Preimpostazioni modem disponibili, l'impostazione predefinita è Lungo veloce." } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "利用可能なモデムプリセット、デフォルトは Long Fast です。" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -5149,6 +5167,12 @@ "value" : "Il pin BLE deve essere composto da 6 cifre." } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "BLE PINは6桁である必要があります。" + } + }, "pl" : { "stringUnit" : { "state" : "translated", @@ -5811,6 +5835,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" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "この機能を有効にすることで、お客様のデバイスのリアルタイム地理位置が暗号化されずにMQTTプロトコル経由で送信されることを承知し、明示的に同意することを認めます。この位置データは、ライブマップ報告、デバイス追跡、関連テレメトリー機能などの目的で使用される場合があります。" + } + }, "zh-Hant-TW" : { "stringUnit" : { "state" : "translated", @@ -7142,6 +7172,12 @@ "value" : "Utilizzo del canale" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "チャンネル使用率" + } + }, "pl" : { "stringUnit" : { "state" : "translated", @@ -7182,6 +7218,12 @@ "value" : "Utilizzo del canale %@%%" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "チャンネル使用率 %@%%" + } + }, "zh-Hant-TW" : { "stringUnit" : { "state" : "translated", @@ -7262,6 +7304,12 @@ "value" : "I canali aggiunti dal codice QR non venivano salvati. Quando si aggiungono canali, i nomi devono essere unici." } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "QRコードから追加されたチャンネルが保存されませんでした。チャンネルを追加する際は、名前が一意である必要があります。" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -8452,6 +8500,12 @@ "value" : "Connessione fallita dopo %d tentativi di connessione a %@. Potrebbe essere necessario disaccoppiare il tuo dispositivo in Impostazioni > Bluetooth." } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "%@ への接続が%d回の試行後に失敗しました。設定 > Bluetooth でデバイスを削除する必要があるかもしれません。" + } + }, "pl" : { "stringUnit" : { "state" : "translated", @@ -9140,6 +9194,12 @@ "value" : "Mostra i moduli che potrebbero non essere supportati al momento da questo nodo." } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "現在、このノードでサポートされていない可能性のあるモジュールを表示しています。" + } + }, "zh-Hant-TW" : { "stringUnit" : { "state" : "translated", @@ -9156,6 +9216,12 @@ "value" : "Attualmente il modo consigliato per aggiornare i dispositivi ESP32 è quello di utilizzare il flasher web su un computer desktop da un browser basato su chrome. Non funziona su dispositivi mobili o tramite BLE." } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "現在、ESP32デバイスの更新の推奨方法は、デスクトップコンピューターのChromeベースのブラウザーでWebフラッシャーを使用することです。モバイルデバイスやBLE経由では動作しません。" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -9442,6 +9508,12 @@ "value" : "Layout dello schermo 128x64 predefinito" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "デフォルト128x64スクリーンレイアウト" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -9642,6 +9714,12 @@ "value" : "Cancellare tutte le metriche del dispositivo?" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "すべてのデバイスメトリクスを削除しますか?" + } + }, "pl" : { "stringUnit" : { "state" : "translated", @@ -10465,6 +10543,12 @@ "value" : "Configurazione dispositivo ricevuta: %@" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "デバイス設定を受信しました: %@" + } + }, "pl" : { "stringUnit" : { "state" : "translated", @@ -10591,6 +10675,12 @@ "value" : "Il dispositivo è gestito da un amministratore di rete, ma l'utente non può accedere alle impostazioni del dispositivo." } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "デバイスはメッシュ管理者によって管理されており、ユーザーはデバイス設定にアクセスできません。" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -11583,6 +11673,12 @@ "value" : "Visualizzazione della configurazione ricevuta: %@" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "ディスプレイ設定を受信しました: %@" + } + }, "pl" : { "stringUnit" : { "state" : "translated", @@ -11623,6 +11719,12 @@ "value" : "Display Fahrenheit" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "華氏表示" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -11651,6 +11753,12 @@ "value" : "Modalità di visualizzazione" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "表示モード" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -11679,6 +11787,12 @@ "value" : "Unità di visualizzazione" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "表示単位" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -11997,6 +12111,12 @@ "value" : "Il Drag & Drop è il metodo consigliato per aggiornare il firmware dei dispositivi NRF. Se l'iPhone o l'iPad è USB-C funzionerà con il normale cavo di ricarica USB-C, mentre per i dispositivi lightning è necessario l'adattatore Apple Lightning to USB camera." } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "ドラッグ&ドロップはNRFデバイスのファームウェア更新に推奨される方法です。お使いのiPhoneまたはiPadがUSB-Cの場合、通常のUSB-C充電ケーブルで動作します。Lightningデバイスの場合は、Apple Lightning to USBカメラアダプターが必要です。" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -13657,6 +13777,12 @@ "value" : "Configurazione del modulo di notifica esterno ricevuta: %@" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "外部通知モジュール設定を受信しました: %@" + } + }, "pl" : { "stringUnit" : { "state" : "translated", @@ -16875,6 +17001,12 @@ "value" : "Con quale frequenza vengono inviate le metriche dei sensori sulla rete. L'impostazione predefinita è 30 minuti." } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "環境メトリクスがメッシュ経由で送信される頻度。デフォルトは30分です。" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -16903,6 +17035,12 @@ "value" : "Con quale frequenza vengono inviate le metriche di potenza attraverso la rete. L'impostazione predefinita è 30 minuti." } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "電力メトリクスがメッシュ経由で送信される頻度。デフォルトは30分です。" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -16965,6 +17103,12 @@ "value" : "Con quale frequenza inviare lo stato del sensore di rilevamento alla rete, indipendentemente dal rilevamento. L'impostazione predefinita è Mai." } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "検出の有無に関係なく、検出センサーの状態をメッシュに送信する頻度。デフォルトは「なし」です。" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -17455,6 +17599,12 @@ "value" : "Se abilitato, il pin di 'uscita' sarà tirato attivo alto, mentre se disabilitato significa attivo basso." } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "有効にすると、「出力」ピンがアクティブハイになり、無効にするとアクティブローになります。" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -17619,6 +17769,12 @@ "value" : "Ignorare il nodo" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "ノードを無視" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -17675,6 +17831,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" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "Local Onlyのように外部メッシュからの観測メッセージを無視しますが、さらに進んで、ノードの既知リストにないノードからのメッセージも無視します。" + } + }, "zh-Hant-TW" : { "stringUnit" : { "state" : "translated", @@ -17685,6 +17847,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" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "オープンまたは復号化できない外部メッシュからの観測メッセージを無視します。ノードのローカル主要/副次チャンネルでのみメッセージを再ブロードキャストします。" + } + }, "zh-Hant-TW" : { "stringUnit" : { "state" : "translated", @@ -17947,6 +18115,12 @@ "value" : "Nodo infrastrutturale solo su una torre o sulla cima di una montagna. Non deve essere utilizzato per tetti o nodi mobili. Necessita di una copertura eccezionale. Visibile nell'elenco dei nodi." } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "タワーまたは山頂のみのインフラストラクチャーノード。屋根や移動ノードには使用しないでください。優れたカバレッジが必要です。ノードリストに表示されます。" + } + }, "pl" : { "stringUnit" : { "state" : "translated", @@ -18005,6 +18179,12 @@ "value" : "Nodo infrastrutturale solo su una torre o sulla cima di una montagna. Non deve essere utilizzato per tetti o nodi mobili. Trasmette i messaggi con un overhead minimo. Non visibile nell'elenco dei nodi." } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "タワーまたは山頂のみのインフラストラクチャーノード。屋根や移動ノードには使用しないでください。最小限のオーバーヘッドでメッセージを中継します。ノードリストには表示されません。" + } + }, "pl" : { "stringUnit" : { "state" : "translated", @@ -18051,6 +18231,12 @@ "value" : "Nodo infrastruttura che ritrasmette sempre i pacchetti una volta, ma solo dopo tutte le altre modalità, garantendo una copertura aggiuntiva per i cluster locali. Visibile nell'elenco dei nodi." } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "常にパケットを一度だけ再ブロードキャストするインフラストラクチャーノードですが、他のすべてのモードの後にのみ実行し、ローカルクラスターの追加カバレッジを確保します。ノードリストに表示されます。" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -19163,6 +19349,12 @@ "value" : "Premere a lungo per privilegiare o silenziare il contatto o eliminare una conversazione." } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "長押しで連絡先をお気に入りに追加、ミュート、または会話を削除できます。" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -19481,6 +19673,12 @@ "value" : "Configurazione LoRa ricevuta: %@" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "LoRa設定を受信しました: %@" + } + }, "pl" : { "stringUnit" : { "state" : "translated", @@ -19601,6 +19799,12 @@ "value" : "M5 Stack Card KB / Tastiera RAK" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "M5 Stack Card KB / RAK キーパッド" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -20443,6 +20647,12 @@ "value" : "Messaggio ricevuto dall'app messaggi di testo." } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "テキストメッセージアプリからメッセージを受信しました。" + } + }, "pl" : { "stringUnit" : { "state" : "translated", @@ -20882,6 +21092,12 @@ "value" : "Tempo minimo tra le trasmissioni di rilevamento" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "検出ブロードキャスト間の最小時間" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -20904,6 +21120,12 @@ "value" : "Tempo minimo tra le trasmissioni di rilevamento. L'impostazione predefinita è 45 secondi." } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "検出ブロードキャスト間の最小時間。デフォルトは45秒です。" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -21174,6 +21396,12 @@ "value" : "La maggior parte dei dati sulla rete viene inviata attraverso il canale principale. È possibile impostare canali secondari per creare gruppi di messaggistica aggiuntivi protetti da una propria chiave. [Suggerimenti per la configurazione del canale](https://meshtastic.org/docs/configuration/tips/)" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "メッシュ上のほとんどのデータはプライマリチャンネル経由で送信されます。セカンダリチャンネルを設定して、独自のキーで保護された追加のメッセージグループを作成できます。[チャンネル設定のヒント](https://meshtastic.org/docs/configuration/tips/)" + } + }, "pl" : { "stringUnit" : { "state" : "translated", @@ -22880,6 +23108,12 @@ "value" : "Backup dei dati del nucleo del nodo %1$@/%2$@ - %3$@ - %4$@" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "ノードコアデータバックアップ %1$@/%2$@ - %3$@ - %4$@" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -23022,6 +23256,12 @@ "value" : "Ricevute informazioni sul nodo per: %@" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "ノード情報を受信しました: %@" + } + }, "pl" : { "stringUnit" : { "state" : "translated", @@ -23102,6 +23342,12 @@ "value" : "Numero di nodo" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "ノード番号" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -23200,6 +23446,12 @@ "value" : "Nodi (%@)" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "ノード (%@)" + } + }, "pl" : { "stringUnit" : { "state" : "translated", From 8acbeb9fc3ff7e103559405c706699cd815cf4fe Mon Sep 17 00:00:00 2001 From: kanakonagiri Date: Tue, 24 Jun 2025 16:49:24 +0900 Subject: [PATCH 173/213] =?UTF-8?q?add:=20=E6=97=A5=E6=9C=AC=E8=AA=9E?= =?UTF-8?q?=E8=A8=B3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Localizable.xcstrings | 444 +++++++++++++++++++++++++++++++++++++++++- 1 file changed, 442 insertions(+), 2 deletions(-) diff --git a/Localizable.xcstrings b/Localizable.xcstrings index f8d14a08..88ce2249 100644 --- a/Localizable.xcstrings +++ b/Localizable.xcstrings @@ -5914,7 +5914,15 @@ } }, "Bytes Used" : { - "comment" : "VoiceOver value for bytes used" + "comment" : "VoiceOver value for bytes used", + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "使用バイト" + } + } + } }, "Call Sign" : { "localizations" : { @@ -8503,7 +8511,7 @@ "ja" : { "stringUnit" : { "state" : "translated", - "value" : "%@ への接続が%d回の試行後に失敗しました。設定 > Bluetooth でデバイスを削除する必要があるかもしれません。" + "value" : "%d への接続が %@ 回の試行後に失敗しました。設定 > Bluetooth でデバイスを削除する必要があるかもしれません。" } }, "pl" : { @@ -24072,6 +24080,12 @@ "value" : "Per l'attivazione degli operatori con licenza è necessario il firmware 2.0.20 o superiore. Assicuratevi di consultare le normative locali e di contattare i coordinatori delle frequenze amatoriali locali per eventuali domande." } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "ライセンス取得者のオンボーディングにはファームウェア2.0.20以上が必要です。必ずお住まいの地域の規制を参照し、疑問がある場合は地域のアマチュア周波数コーディネーターにお問い合わせください。" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -24320,6 +24334,12 @@ }, "Only permitted for SENSOR, TRACKER and TAK_TRACKER roles, this will inhibit all rebroadcasts, not unlike CLIENT_MUTE role." : { "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "SENSOR、TRACKER、TAK_TRACKERロールでのみ許可されており、CLIENT_MUTEロールと同様にすべての再ブロードキャストを抑制します。" + } + }, "zh-Hant-TW" : { "stringUnit" : { "state" : "translated", @@ -24370,6 +24390,12 @@ "value" : "Codice di localizzazione aperto (alias Codice Plus)" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "オープンロケーションコード(プラスコード)" + } + }, "pl" : { "stringUnit" : { "state" : "translated", @@ -24416,6 +24442,12 @@ "value" : "Aprire le impostazioni" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "設定を開く" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -24536,6 +24568,12 @@ "value" : "Campi opzionali da includere quando si assemblano i messaggi di posizione. Più campi sono inclusi, più grande sarà il messaggio, con conseguente allungamento dei tempi di trasmissione e un maggiore rischio di perdita di pacchetti" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "位置メッセージを組み立てる際に含めるオプションフィールド。含めるフィールドが多いほどメッセージが大きくなり、通信時間が長くなってパケット損失のリスクが高くなります。" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -25886,6 +25924,12 @@ "value" : "Cacca" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "うんち" + } + }, "pl" : { "stringUnit" : { "state" : "translated", @@ -26206,6 +26250,12 @@ "value" : "Posizione Log %lld Punti" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "位置ログ %lld ポイント" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -26274,6 +26324,12 @@ "value" : "Posizione Pacchetto ricevuto dal nodo: %@" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "ノード %@ から位置パケットを受信しました" + } + }, "pl" : { "stringUnit" : { "state" : "translated", @@ -28740,6 +28796,12 @@ "value" : "Richiesta amministratore legacy: %@" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "レガシー管理者要求: %@" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -28762,6 +28824,12 @@ "value" : "Richiesta PKI Admin: %@" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "PKI管理者要求: %@" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -28796,6 +28864,12 @@ "value" : "Messaggi in scatola richiesti Messaggi del modulo per il nodo: %@" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "ノード %@ の定型メッセージモジュールメッセージを要求しました" + } + }, "pl" : { "stringUnit" : { "state" : "translated", @@ -28870,6 +28944,12 @@ "value" : "Ripristino delle impostazioni dell'app" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "アプリ設定をリセット" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -28904,6 +28984,12 @@ "value" : "Azzeramento di NodeDB" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "NodeDBをリセット" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -29340,6 +29426,12 @@ "value" : "Ringtone Transfer Language(RTTTL) Stringa di suoneria utilizzata dai cicalini supportati nelle notifiche esterne." } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "外部通知でサポートされているブザーで使用されるRingtone Transfer Language(RTTTL)着信音文字列。" + } + }, "pl" : { "stringUnit" : { "state" : "translated", @@ -29420,6 +29512,12 @@ "value" : "Ruolo: %@" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "役割: %@" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -29538,6 +29636,12 @@ "value" : "Percorso di ritorno: %@" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "ルート(復路): %@" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -29560,6 +29664,12 @@ "value" : "Linee di percorso" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "ルートライン" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -29582,6 +29692,12 @@ "value" : "Registratore di percorso" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "ルートレコーダー" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -29604,6 +29720,12 @@ "value" : "Registrazione del percorso in pausa" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "ルート記録を一時停止" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -29780,6 +29902,12 @@ "value" : "Routing ricevuto per RequestID: %@ Ack Status: %@" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "リクエストID %@ のルーティングを受信、応答ステータス: %@" + } + }, "pl" : { "stringUnit" : { "state" : "translated", @@ -29922,6 +30050,12 @@ "value" : "RTTTL Configurazione suoneria ricevuta: %@" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "RTTTL着信音設定を受信しました: %@" + } + }, "pl" : { "stringUnit" : { "state" : "translated", @@ -30006,6 +30140,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" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "ALLと同じ動作ですが、パケットのデコードをスキップして単純に再ブロードキャストします。リピーター役割でのみ利用可能です。他の役割で設定するとALLの動作になります。" + } + }, "zh-Hant-TW" : { "stringUnit" : { "state" : "translated", @@ -31124,6 +31264,12 @@ "value" : "Invia ${messaggioContenuto} a ${canaleNumero}" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "${messageContent} をチャンネル ${channelNumber} に送信" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -31146,6 +31292,12 @@ "value" : "Invia ${messaggioContenuto} a ${nodoNumero}" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "${messageContent} をノード ${nodeNumber} に送信" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -31230,6 +31382,12 @@ "value" : "Invia un heartbeat per pubblicizzare la presenza del server." } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "サーバーの存在を通知するためのハートビートを送信します。" + } + }, "zh-Hant-TW" : { "stringUnit" : { "state" : "translated", @@ -31246,6 +31404,12 @@ "value" : "Inviare un messaggio a un certo canale meshtastic" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "特定のMeshtasticチャンネルにメッセージを送信" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -31268,6 +31432,12 @@ "value" : "Inviare un messaggio a un certo nodo meshtastico" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "特定のMeshtasticノードにメッセージを送信" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -31290,6 +31460,12 @@ "value" : "Invia una posizione sul canale principale quando si fa triplo clic sul pulsante utente." } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "ユーザーボタンが3回クリックされたときにプライマリチャンネルで位置を送信します。" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -31358,6 +31534,12 @@ "value" : "Inviare un waypoint" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "ウェイポイントを送信" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -31380,6 +31562,12 @@ "value" : "Invia una campana ASCII con un messaggio di avviso. Utile per attivare notifiche esterne alla ricezione della campana." } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "アラートメッセージ付きASCIIベルを送信。ベルでの外部通知のトリガーに便利です。" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -31752,6 +31940,12 @@ "value" : "Inviato un LoRa.Config per: %@" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "%@ にLoRa設定を送信しました" + } + }, "pl" : { "stringUnit" : { "state" : "translated", @@ -31811,6 +32005,12 @@ "value" : "Inviato un pacchetto di posizione dal GPS del dispositivo Apple al nodo: %@" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "AppleデバイスのGPSからノード %@ に位置パケットを送信しました" + } + }, "pl" : { "stringUnit" : { "state" : "translated", @@ -31869,6 +32069,12 @@ "value" : "Ha inviato una richiesta di tracciamento della rotta al nodo: %@" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "ノード %@ にトレースルート要求を送信しました" + } + }, "pl" : { "stringUnit" : { "state" : "translated", @@ -31927,6 +32133,12 @@ "value" : "Inviato un pacchetto Waypoint da: %@" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "%@ からウェイポイントパケットを送信しました" + } + }, "pl" : { "stringUnit" : { "state" : "translated", @@ -31985,6 +32197,12 @@ "value" : "Inviato messaggio %@ da %@ a %@" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "メッセージ %@ を %@ から %@ に送信しました" + } + }, "pl" : { "stringUnit" : { "state" : "translated", @@ -32065,6 +32283,12 @@ "value" : "Sequenza: %@" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "シーケンス: %@" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -32249,6 +32473,12 @@ "value" : "Console seriale tramite l'API Stream." } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "Stream API経由のシリアルコンソール。" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -32583,6 +32813,12 @@ "value" : "Imposta il numero massimo di hop, l'impostazione predefinita è 3. L'aumento degli hop aumenta anche la congestione e deve essere usato con attenzione. I messaggi di broadcasting a un hop non riceveranno ACK." } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "最大ホップ数を設定します。デフォルトは3です。ホップ数を増やすと輻輳も増加するため、慎重に使用してください。0ホップのブロードキャストメッセージはACKを受信しません。" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -33221,6 +33457,12 @@ "value" : "Mostra sulla mappa della mesh." } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "メッシュマップに表示。" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -34267,6 +34509,12 @@ "value" : "Salva & Inoltra" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "蓄積転送" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -34295,6 +34543,12 @@ "value" : "Configurazione Salva & Inoltra" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "蓄積転送設定" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -34335,6 +34589,12 @@ "value" : "Configurazione del modulo Store & Forward ricevuta: %@" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "蓄積転送モジュール設定を受信しました: %@" + } + }, "pl" : { "stringUnit" : { "state" : "translated", @@ -34375,6 +34635,12 @@ "value" : "I server Store and Forward richiedono un dispositivo ESP32 con PSRAM o Linux Native." } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "蓄積転送サーバーには、PSRAM搭載のESP32デバイスまたはLinux Nativeが必要です。" + } + }, "zh-Hant-TW" : { "stringUnit" : { "state" : "translated", @@ -34487,6 +34753,12 @@ "value" : "I sensori I2C supportati vengono rilevati automaticamente: BMP280, BME280, BME680, MCP9808, INA219, INA260, LPS22 e SHTC3." } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "サポートされているI2C接続センサーは自動的に検出されます。センサーはBMP280、BME280、BME680、MCP9808、INA219、INA260、LPS22、およびSHTC3です。" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -34899,6 +35171,12 @@ "value" : "Configurazione del modulo di telemetria ricevuta: %@" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "テレメトリーモジュール設定を受信しました: %@" + } + }, "pl" : { "stringUnit" : { "state" : "translated", @@ -34957,6 +35235,12 @@ "value" : "Telemetria ricevuta per: %@" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "%@ のテレメトリーを受信しました" + } + }, "pl" : { "stringUnit" : { "state" : "translated", @@ -35635,6 +35919,12 @@ "value" : "La chiave pubblica più recente di questo nodo non corrisponde alla chiave registrata in precedenza. È possibile eliminare il nodo e fargli scambiare nuovamente le chiavi, ma questo potrebbe indicare un problema di sicurezza più serio. Contattare l'utente attraverso un altro canale fidato per determinare se la modifica della chiave è dovuta a un reset di fabbrica o a un'altra azione intenzionale." } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "このノードの最新の公開キーが以前記録されたキーと一致しません。ノードを削除して再度キー交換を行うことができますが、これはより深刻なセキュリティ問題を示している可能性もあります。信頼できる別のチャンネルを通じてユーザーに連絡し、キーの変更が工場リセットやその他の intentional action によるものかどうかを確認してください。" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -35767,6 +36057,12 @@ "value" : "La chiave pubblica non corrisponde alla chiave registrata. È possibile eliminare il nodo e fargli scambiare nuovamente le chiavi, ma questo potrebbe indicare un problema di sicurezza più serio. Contattare l'utente attraverso un altro canale fidato, per determinare se la modifica della chiave è dovuta a un reset di fabbrica o a un'altra azione intenzionale." } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "公開キーが記録されたキーと一致しません。ノードを削除して再度キー交換を行うことができますが、これはより深刻なセキュリティ問題を示している可能性があります。信頼できる別のチャンネルを通じてユーザーに連絡し、キーの変更が工場リセットやその他の意図的な操作によるものかどうか確認してください。" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -35863,6 +36159,12 @@ "value" : "I ruoli di router sono progettati per posizioni elevate, come le cime delle montagne e le torri. Questo nodo deve essere in grado di avere una buona connessione diretta con la maggior parte dei nodi della rete, altrimenti danneggia significativamente la rete." } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "ルーター役割は山頂や塔のような見晴らしの良い高所での使用を想定して設計されています。このノードは、ネットワーク内の大部分のノードと良好な直接接続を保持できる必要があります。そうでなければ、ネットワークに深刻な影響を与えることになります。" + } + }, "zh-Hant-TW" : { "stringUnit" : { "state" : "translated", @@ -35981,6 +36283,12 @@ "value" : "La chiave pubblica terziaria autorizzata a inviare messaggi di amministrazione a questo nodo." } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "このノードに管理メッセージを送信する権限を持つ三次公開キー。" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -36009,6 +36317,12 @@ "value" : "L'URL per le impostazioni del canale" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "チャンネル設定のURL" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -36025,6 +36339,12 @@ }, "The URL for the node to add" : { "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "追加するノードのURL" + } + }, "zh-Hant-TW" : { "stringUnit" : { "state" : "translated", @@ -36041,6 +36361,12 @@ "value" : "Non è stata data risposta a una richiesta di metadati del dispositivo sul canale di amministrazione per questo nodo." } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "このノードの管理チャンネル経由でのデバイスメタデータ要求に対する応答がありません。" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -36063,6 +36389,12 @@ "value" : "Queste impostazioni saranno %@ canali. La configurazione LoRa corrente verrà sostituita; se vengono apportate modifiche sostanziali alla configurazione LoRa, il dispositivo si riavvierà" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "これらの設定はチャンネルを%@します。現在のLoRa設定は置き換えられ、LoRa設定に大幅な変更があった場合、デバイスは再起動します" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -36305,6 +36637,12 @@ "value" : "La risposta potrebbe richiedere un po' di tempo e verrà visualizzata nel registro delle rotte di tracciamento per il nodo a cui è stata inviata." } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "これには時間がかかる場合があります。応答は送信先ノードのトレースルートログに表示されます。" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -36327,6 +36665,12 @@ "value" : "Il dispositivo invia messaggi di test di portata all'intervallo selezionato." } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "このデバイスは選択した間隔でレンジテストメッセージを送信します。" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -36411,6 +36755,12 @@ "value" : "In questo modo si disattiva la posizione fissa e si rimuove la posizione attualmente impostata." } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "これにより固定位置が無効になり、現在設定されている位置が削除されます。" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -36433,6 +36783,12 @@ "value" : "In questo modo si invia la posizione corrente dal telefono e si abilita la posizione fissa." } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "これにより、お使いの携帯電話から現在位置を送信し、固定位置を有効にします。" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -36607,6 +36963,12 @@ "value" : "Pollici in giù" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "👎" + } + }, "pl" : { "stringUnit" : { "state" : "translated", @@ -36665,6 +37027,12 @@ "value" : "Pollici in su" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "👍" + } + }, "pl" : { "stringUnit" : { "state" : "translated", @@ -36813,6 +37181,12 @@ "value" : "Fuso orario per le date sullo schermo del dispositivo e sul registro." } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "デバイス画面とログの日付用タイムゾーン。" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -37019,6 +37393,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" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "CCPAやGDPRなどのプライバシー法に準拠するため、正確な位置データの共有は避けています。代わりに、あなたのプライバシーを保護するために匿名化または近似(不正確)の位置情報を使用します。" + } + }, "zh-Hant-TW" : { "stringUnit" : { "state" : "translated", @@ -37035,6 +37415,12 @@ "value" : "Argomento: %@" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "トピック: %@" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -37147,6 +37533,12 @@ }, "Trace Route (in %@s)" : { "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "トレースルート(%@秒)" + } + }, "zh-Hant-TW" : { "stringUnit" : { "state" : "translated", @@ -37209,6 +37601,12 @@ "value" : "Traccia Richiesta di rotta restituita: %@" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "トレースルート要求が返されました: %@" + } + }, "pl" : { "stringUnit" : { "state" : "translated", @@ -37277,6 +37675,12 @@ "value" : "Traccia del percorso inviato a %@" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "%@ にトレースルートを送信しました" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -37299,6 +37703,12 @@ "value" : "La rotta di tracciamento verso %@ non è stata inviata." } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "%@ へのトレースルートは送信されませんでした。" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -39811,6 +40221,12 @@ "value" : "In attesa di essere riconosciuti. . ." } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "承認待ち. . ." + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -39833,6 +40249,12 @@ "value" : "Svegliare lo schermo al tocco o al movimento" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "タップまたはモーションで画面を起動" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -39997,6 +40419,12 @@ "value" : "Pacchetto Waypoint ricevuto dal nodo: %@" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "ノードから受信したウェイポイント パケット: %@" + } + }, "pl" : { "stringUnit" : { "state" : "translated", @@ -40281,6 +40709,12 @@ "value" : "Quando è abilitato, il modulo PAX Counter conta il numero di persone che passano utilizzando il WiFi e il Bluetooth. Per il funzionamento del contatore PAX, sia il WiFI che il Bluetooth devono essere disattivati." } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "PAXカウンターモジュールを有効にすると、WiFiとBluetoothを使用して通過する人数をカウントします。PAXカウンターを動作させるには、WiFiとBluetoothの両方を無効にする必要があります。" + } + }, "se" : { "stringUnit" : { "state" : "translated", @@ -40451,6 +40885,12 @@ "value" : "Sospenderà tutto il più possibile, per il ruolo di tracker e sensore questo includerà anche la radio lora. Non utilizzare questa impostazione se si desidera utilizzare il dispositivo con le applicazioni del telefono o se si utilizza un dispositivo senza pulsante utente." } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "トラッカーとセンサーの役割では、lora無線も含め、すべてのデバイスを可能な限りスリープ状態にします。デバイスをスマートフォンアプリと連携させたい場合、またはユーザーボタンのないデバイスを使用している場合は、この設定を使用しないでください。" + } + }, "pl" : { "stringUnit" : { "state" : "translated", From 344b7780e00a5f7d59b3f901388b3127f524e698 Mon Sep 17 00:00:00 2001 From: kanakonagiri Date: Tue, 24 Jun 2025 16:53:59 +0900 Subject: [PATCH 174/213] =?UTF-8?q?add:=20=E6=97=A5=E6=9C=AC=E8=AA=9E?= =?UTF-8?q?=E8=A8=B3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Localizable.xcstrings | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/Localizable.xcstrings b/Localizable.xcstrings index 88ce2249..f1044d13 100644 --- a/Localizable.xcstrings +++ b/Localizable.xcstrings @@ -37731,6 +37731,12 @@ "value" : "La rotta di traccia era limitata dalla velocità. È possibile inviare una rotta di tracciamento al massimo una volta ogni trenta secondi." } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "トレースルートの送信レートが制限されました。トレースルートは30秒ごとに最大1回送信できます。" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -39313,6 +39319,12 @@ "value" : "Utilizzare un'uscita PWM (come il cicalino RAK) per le sintonie invece di un'uscita on/off. In questo modo si ignorano le impostazioni di uscita, durata e attivazione e si utilizza invece l'opzione GPIO del cicalino configurata dal dispositivo." } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "オン/オフ出力ではなく、PWM出力(RAKブザーなど)をチューンに使用してください。これにより、出力、出力時間、アクティブ設定は無視され、代わりにデバイス設定のブザーGPIOオプションが使用されます。" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -39447,6 +39459,12 @@ "value" : "Si usa per creare una chiave condivisa con un dispositivo remoto." } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "リモート デバイスとの共有キーを作成するために使用されます。" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -39879,6 +39897,12 @@ "value" : "La versione %1$@ include sostanziali ottimizzazioni di rete e modifiche estese ai dispositivi e alle applicazioni client. Sono supportati solo i nodi versione %2$@ e superiori." } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "バージョン%1$@には、ネットワークの大幅な最適化と、デバイスおよびクライアントアプリへの広範な変更が含まれています。サポートされるノードはバージョン%2$@以降のみです。" + } + }, "sr" : { "stringUnit" : { "state" : "translated", From aa73a82404da25f88c9099bf55e100ebd28982bb Mon Sep 17 00:00:00 2001 From: dborup Date: Tue, 24 Jun 2025 17:18:04 +0200 Subject: [PATCH 175/213] Add TracerouteIntent for iOS Shortcuts integration This commit adds a new AppIntent called TracerouteIntent, allowing users to send a traceroute request to a specific Meshtastic node directly via iOS Shortcuts or Siri. The intent calls BLEManager.shared.sendTraceRouteRequest(destNum:wantResponse:) and provides basic validation to ensure the device is connected. No other files or logic were modified. This follows the same structural pattern as MessageNodeIntent.swift. --- Meshtastic/AppIntents/TracerouteIntent.swift | 27 ++++++++++++++++++++ 1 file changed, 27 insertions(+) create mode 100644 Meshtastic/AppIntents/TracerouteIntent.swift diff --git a/Meshtastic/AppIntents/TracerouteIntent.swift b/Meshtastic/AppIntents/TracerouteIntent.swift new file mode 100644 index 00000000..99c19348 --- /dev/null +++ b/Meshtastic/AppIntents/TracerouteIntent.swift @@ -0,0 +1,27 @@ +import Foundation +import AppIntents + +struct TracerouteIntent: AppIntent { + static var title: LocalizedStringResource = "Send a Traceroute" + + static var description: IntentDescription = "Send a traceroute request to a certain Meshtastic node" + + @Parameter(title: "Node Number") + var nodeNumber: Int + + static var parameterSummary: some ParameterSummary { + Summary("Send traceroute to \(\.$nodeNumber)") + } + + func perform() async throws -> some IntentResult { + if !BLEManager.shared.isConnected { + throw AppIntentErrors.AppIntentError.notConnected + } + + if !BLEManager.shared.sendTraceRouteRequest(destNum: Int64(nodeNumber), wantResponse: true) { + throw AppIntentErrors.AppIntentError.message("Failed to send traceroute request") + } + + return .result() + } +} From d78ab886009ac4912cbfbf0183a04264335c7758 Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Thu, 26 Jun 2025 07:16:06 -0700 Subject: [PATCH 176/213] Bump version --- Localizable.xcstrings | 2 +- Meshtastic.xcodeproj/project.pbxproj | 8 ++++---- Meshtastic/Views/Nodes/Helpers/NodeInfoItem.swift | 2 +- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/Localizable.xcstrings b/Localizable.xcstrings index ba0d7976..3be1addc 100644 --- a/Localizable.xcstrings +++ b/Localizable.xcstrings @@ -15045,7 +15045,7 @@ } } }, - "incomplete" : { + "Incomplete" : { "localizations" : { "de" : { "stringUnit" : { diff --git a/Meshtastic.xcodeproj/project.pbxproj b/Meshtastic.xcodeproj/project.pbxproj index 55581bc9..d18f130e 100644 --- a/Meshtastic.xcodeproj/project.pbxproj +++ b/Meshtastic.xcodeproj/project.pbxproj @@ -1828,7 +1828,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 2.6.9; + MARKETING_VERSION = 2.6.10; PRODUCT_BUNDLE_IDENTIFIER = gvh.MeshtasticClient; PRODUCT_NAME = "$(TARGET_NAME)"; SUPPORTS_MACCATALYST = YES; @@ -1861,7 +1861,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 2.6.9; + MARKETING_VERSION = 2.6.10; PRODUCT_BUNDLE_IDENTIFIER = gvh.MeshtasticClient; PRODUCT_NAME = "$(TARGET_NAME)"; SUPPORTS_MACCATALYST = YES; @@ -1892,7 +1892,7 @@ "@executable_path/Frameworks", "@executable_path/../../Frameworks", ); - MARKETING_VERSION = 2.6.9; + MARKETING_VERSION = 2.6.10; PRODUCT_BUNDLE_IDENTIFIER = gvh.MeshtasticClient.Widgets; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; @@ -1924,7 +1924,7 @@ "@executable_path/Frameworks", "@executable_path/../../Frameworks", ); - MARKETING_VERSION = 2.6.9; + MARKETING_VERSION = 2.6.10; PRODUCT_BUNDLE_IDENTIFIER = gvh.MeshtasticClient.Widgets; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; diff --git a/Meshtastic/Views/Nodes/Helpers/NodeInfoItem.swift b/Meshtastic/Views/Nodes/Helpers/NodeInfoItem.swift index eb4c37b0..3c20a9e2 100644 --- a/Meshtastic/Views/Nodes/Helpers/NodeInfoItem.swift +++ b/Meshtastic/Views/Nodes/Helpers/NodeInfoItem.swift @@ -79,7 +79,7 @@ struct NodeInfoItem: View { if user.hwModel != "UNSET" { Text(String(node.user?.hwDisplayName ?? (node.user?.hwModel ?? "Unset".localized))) } else { - Text(String("incomplete".localized)) + Text(String("Incomplete".localized)) } } .accessibilityElement(children: .combine) From e83347d1d89883638b0253309aa1fa7898af6aa5 Mon Sep 17 00:00:00 2001 From: Ben Meadors Date: Thu, 26 Jun 2025 16:59:29 -0500 Subject: [PATCH 177/213] Fix uint32 overflows and add safeint32 methods for re-use --- Meshtastic/Persistence/UpdateCoreData.swift | 26 ++++++++++++++++----- 1 file changed, 20 insertions(+), 6 deletions(-) diff --git a/Meshtastic/Persistence/UpdateCoreData.swift b/Meshtastic/Persistence/UpdateCoreData.swift index 6d085b21..a42b2937 100644 --- a/Meshtastic/Persistence/UpdateCoreData.swift +++ b/Meshtastic/Persistence/UpdateCoreData.swift @@ -8,6 +8,19 @@ import CoreData import MeshtasticProtobufs import OSLog +// MARK: - Safe Conversion Helpers +private func safeInt32(from value: UInt32) -> Int32 { + return Int32(clamping: value) +} + +private func safeInt32(from value: Int) -> Int32 { + return Int32(clamping: value) +} + +private func safeInt32(from value: UInt64) -> Int32 { + return Int32(clamping: value) +} + public func clearStaleNodes(nodeExpireDays: Int, context: NSManagedObjectContext) -> Bool { var nodeExpireTime: TimeInterval { return TimeInterval(-nodeExpireDays * 86400) @@ -1367,6 +1380,7 @@ func upsertRangeTestModuleConfigPacket(config: ModuleConfig.RangeTestConfig, nod do { try context.save() Logger.data.info("💾 [RangeTestConfigEntity] Updated for node: \(nodeNum.toHex(), privacy: .public)") + } catch { context.rollback() let nsError = error as NSError @@ -1498,23 +1512,23 @@ func upsertTelemetryModuleConfigPacket(config: ModuleConfig.TelemetryConfig, nod if !fetchedNode.isEmpty { if fetchedNode[0].telemetryConfig == nil { let newTelemetryConfig = TelemetryConfigEntity(context: context) - newTelemetryConfig.deviceUpdateInterval = Int32(config.deviceUpdateInterval) - newTelemetryConfig.environmentUpdateInterval = Int32(config.environmentUpdateInterval) + newTelemetryConfig.deviceUpdateInterval = safeInt32(from: config.deviceUpdateInterval) + newTelemetryConfig.environmentUpdateInterval = safeInt32(from: config.environmentUpdateInterval) newTelemetryConfig.environmentMeasurementEnabled = config.environmentMeasurementEnabled newTelemetryConfig.environmentScreenEnabled = config.environmentScreenEnabled newTelemetryConfig.environmentDisplayFahrenheit = config.environmentDisplayFahrenheit newTelemetryConfig.powerMeasurementEnabled = config.powerMeasurementEnabled - newTelemetryConfig.powerUpdateInterval = Int32(config.powerUpdateInterval) + newTelemetryConfig.powerUpdateInterval = safeInt32(from: config.powerUpdateInterval) newTelemetryConfig.powerScreenEnabled = config.powerScreenEnabled fetchedNode[0].telemetryConfig = newTelemetryConfig } else { - fetchedNode[0].telemetryConfig?.deviceUpdateInterval = Int32(config.deviceUpdateInterval) - fetchedNode[0].telemetryConfig?.environmentUpdateInterval = Int32(config.environmentUpdateInterval) + fetchedNode[0].telemetryConfig?.deviceUpdateInterval = safeInt32(from: config.deviceUpdateInterval) + fetchedNode[0].telemetryConfig?.environmentUpdateInterval = safeInt32(from: config.environmentUpdateInterval) fetchedNode[0].telemetryConfig?.environmentMeasurementEnabled = config.environmentMeasurementEnabled fetchedNode[0].telemetryConfig?.environmentScreenEnabled = config.environmentScreenEnabled fetchedNode[0].telemetryConfig?.environmentDisplayFahrenheit = config.environmentDisplayFahrenheit fetchedNode[0].telemetryConfig?.powerMeasurementEnabled = config.powerMeasurementEnabled - fetchedNode[0].telemetryConfig?.powerUpdateInterval = Int32(config.powerUpdateInterval) + fetchedNode[0].telemetryConfig?.powerUpdateInterval = safeInt32(from: config.powerUpdateInterval) fetchedNode[0].telemetryConfig?.powerScreenEnabled = config.powerScreenEnabled } if sessionPasskey != nil { From c014baf9866fb37488a0ec6fa382c9644b9ce3ee Mon Sep 17 00:00:00 2001 From: Ben Meadors Date: Fri, 27 Jun 2025 06:26:24 -0500 Subject: [PATCH 178/213] Add sync device svgs job --- .github/workflows/sync_device_svgs.yml | 161 +++++++++++++++++++++++++ 1 file changed, 161 insertions(+) create mode 100644 .github/workflows/sync_device_svgs.yml diff --git a/.github/workflows/sync_device_svgs.yml b/.github/workflows/sync_device_svgs.yml new file mode 100644 index 00000000..e1a1fd94 --- /dev/null +++ b/.github/workflows/sync_device_svgs.yml @@ -0,0 +1,161 @@ +name: Sync Device SVGs + +on: + schedule: + # Run nightly at 2 AM UTC + - cron: '0 2 * * *' + workflow_dispatch: + # Allow manual triggering + +jobs: + sync-device-svgs: + runs-on: ubuntu-latest + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + with: + token: ${{ secrets.GITHUB_TOKEN }} + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: '18' + + - name: Install dependencies + run: | + npm install -g svgo + + - name: Download and process SVGs + run: | + #!/bin/bash + set -e + + # Create temporary directory + mkdir -p temp_svgs + cd temp_svgs + + # Clone web-flasher repo (shallow clone for speed) + git clone --depth 1 https://github.com/meshtastic/web-flasher.git + + # Navigate to SVG directory + cd web-flasher/public/img/devices + + # Create output directory + mkdir -p ../../../../processed_svgs + + # Process each SVG file + for svg_file in *.svg; do + if [ -f "$svg_file" ]; then + # Get filename without extension + filename=$(basename "$svg_file" .svg) + + # Optimize SVG + svgo "$svg_file" --output "../../../../processed_svgs/${filename}.svg" + + echo "Processed: $filename" + fi + done + + cd ../../../../ + ls -la processed_svgs/ + + - name: Update Xcode Assets + run: | + #!/bin/bash + set -e + + ASSETS_DIR="Meshtastic/Assets.xcassets" + + # Ensure assets directory exists + mkdir -p "$ASSETS_DIR" + + # Process each SVG + for svg_file in processed_svgs/*.svg; do + if [ -f "$svg_file" ]; then + # Get filename without extension + filename=$(basename "$svg_file" .svg) + + # Create imageset directory + imageset_dir="${ASSETS_DIR}/${filename}.imageset" + mkdir -p "$imageset_dir" + + # Copy SVG to imageset + cp "$svg_file" "${imageset_dir}/${filename}.svg" + + # Create Contents.json for the imageset + cat > "${imageset_dir}/Contents.json" << EOF + { + "images" : [ + { + "filename" : "${filename}.svg", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : true + } + } + EOF + + echo "Created imageset: ${filename}" + fi + done + + - name: Check for changes + id: check_changes + run: | + if git diff --quiet; then + echo "has_changes=false" >> $GITHUB_OUTPUT + else + echo "has_changes=true" >> $GITHUB_OUTPUT + fi + + - name: Commit and push changes + if: steps.check_changes.outputs.has_changes == 'true' + run: | + git config --local user.email "action@github.com" + git config --local user.name "GitHub Action" + git add Meshtastic/Assets.xcassets/ + git commit -m "🤖 Sync device SVGs from web-flasher repo + + - Updated device images from meshtastic/web-flasher + - Automatically synced on $(date -u) + - Source: https://github.com/meshtastic/web-flasher/tree/main/public/img/devices" + git push + + - name: Create PR (alternative to direct push) + if: steps.check_changes.outputs.has_changes == 'true' && false # Set to true if you prefer PRs + uses: peter-evans/create-pull-request@v5 + with: + token: ${{ secrets.GITHUB_TOKEN }} + commit-message: "🤖 Sync device SVGs from web-flasher repo" + title: "Sync device SVGs from web-flasher" + body: | + This PR automatically syncs device SVG images from the [meshtastic/web-flasher](https://github.com/meshtastic/web-flasher) repository. + + **Changes:** + - Updated device images from web-flasher repo + - Source: https://github.com/meshtastic/web-flasher/tree/main/public/img/devices + - Automatically generated on $(date -u) + + The SVGs have been optimized and converted to Xcode asset format. + branch: sync-device-svgs + delete-branch: true + + - name: Cleanup + if: always() + run: | + rm -rf temp_svgs processed_svgs + + - name: Summary + run: | + if [ "${{ steps.check_changes.outputs.has_changes }}" == "true" ]; then + echo "✅ Device SVGs updated successfully" + else + echo "ℹ️ No changes detected - SVGs are up to date" + fi \ No newline at end of file From 5c08f815506b876076ac103dcfc389acc8033320 Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Fri, 27 Jun 2025 15:56:03 -0700 Subject: [PATCH 179/213] Check if a user has moved 9 meters for the position log instead of 15 in case they have set the value to 10 meters --- Meshtastic/Persistence/UpdateCoreData.swift | 2 +- protobufs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Meshtastic/Persistence/UpdateCoreData.swift b/Meshtastic/Persistence/UpdateCoreData.swift index 6d085b21..63629137 100644 --- a/Meshtastic/Persistence/UpdateCoreData.swift +++ b/Meshtastic/Persistence/UpdateCoreData.swift @@ -454,7 +454,7 @@ func upsertPositionPacket (packet: MeshPacket, context: NSManagedObjectContext) } /// Don't save nearly the same position over and over. If the next position is less than 10 meters from the new position, delete the previous position and save the new one. if mutablePositions.count > 0 && (position.precisionBits == 32 || position.precisionBits == 0) { - if let mostRecent = mutablePositions.lastObject as? PositionEntity, mostRecent.coordinate.distance(from: position.coordinate) < 15.0 { + if let mostRecent = mutablePositions.lastObject as? PositionEntity, mostRecent.coordinate.distance(from: position.coordinate) < 9.0 { mutablePositions.remove(mostRecent) } } else if mutablePositions.count > 0 { diff --git a/protobufs b/protobufs index 816595c8..27fac391 160000 --- a/protobufs +++ b/protobufs @@ -1 +1 @@ -Subproject commit 816595c8bbdfc3b4388e11348ccd043294d58705 +Subproject commit 27fac39141d99fe727a0a1824c5397409b1aea75 From b47a259337334d9f4785831a26fab0771d3b0c55 Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Fri, 27 Jun 2025 16:03:02 -0700 Subject: [PATCH 180/213] Use 9 meters instead of 15 for position log --- Meshtastic/Persistence/UpdateCoreData.swift | 2 +- protobufs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Meshtastic/Persistence/UpdateCoreData.swift b/Meshtastic/Persistence/UpdateCoreData.swift index a42b2937..bc0093a8 100644 --- a/Meshtastic/Persistence/UpdateCoreData.swift +++ b/Meshtastic/Persistence/UpdateCoreData.swift @@ -467,7 +467,7 @@ func upsertPositionPacket (packet: MeshPacket, context: NSManagedObjectContext) } /// Don't save nearly the same position over and over. If the next position is less than 10 meters from the new position, delete the previous position and save the new one. if mutablePositions.count > 0 && (position.precisionBits == 32 || position.precisionBits == 0) { - if let mostRecent = mutablePositions.lastObject as? PositionEntity, mostRecent.coordinate.distance(from: position.coordinate) < 15.0 { + if let mostRecent = mutablePositions.lastObject as? PositionEntity, mostRecent.coordinate.distance(from: position.coordinate) < 9.0 { mutablePositions.remove(mostRecent) } } else if mutablePositions.count > 0 { diff --git a/protobufs b/protobufs index 816595c8..27fac391 160000 --- a/protobufs +++ b/protobufs @@ -1 +1 @@ -Subproject commit 816595c8bbdfc3b4388e11348ccd043294d58705 +Subproject commit 27fac39141d99fe727a0a1824c5397409b1aea75 From f14f8c97e2611eb37539d3ce46fb33e0509d75f4 Mon Sep 17 00:00:00 2001 From: Benjamin Faershtein <119711889+RCGV1@users.noreply.github.com> Date: Fri, 27 Jun 2025 16:52:51 -0700 Subject: [PATCH 181/213] QR code improvements --- Localizable.xcstrings | 5 +- Meshtastic.xcodeproj/project.pbxproj | 4 + Meshtastic/Helpers/BLEManager.swift | 3 +- Meshtastic/Helpers/ContactURLHandler.swift | 86 ++++++ Meshtastic/MeshtasticApp.swift | 108 ++----- Meshtastic/Views/Messages/MessageText.swift | 58 ++++ .../Views/Settings/SaveChannelQRCode.swift | 292 ++++++++++++++++-- 7 files changed, 453 insertions(+), 103 deletions(-) create mode 100644 Meshtastic/Helpers/ContactURLHandler.swift diff --git a/Localizable.xcstrings b/Localizable.xcstrings index ba0d7976..c8cd9692 100644 --- a/Localizable.xcstrings +++ b/Localizable.xcstrings @@ -16445,6 +16445,9 @@ } } } + }, + "LoRa Config Changes:" : { + }, "LoRa config received: %@" : { "localizations" : { @@ -35088,4 +35091,4 @@ } }, "version" : "1.0" -} \ No newline at end of file +} diff --git a/Meshtastic.xcodeproj/project.pbxproj b/Meshtastic.xcodeproj/project.pbxproj index 55581bc9..3d3b7e7c 100644 --- a/Meshtastic.xcodeproj/project.pbxproj +++ b/Meshtastic.xcodeproj/project.pbxproj @@ -65,6 +65,7 @@ BCB613832C672A2600485544 /* MessageChannelIntent.swift in Sources */ = {isa = PBXBuildFile; fileRef = BCB613822C672A2600485544 /* MessageChannelIntent.swift */; }; BCB613852C68703800485544 /* NodePositionIntent.swift in Sources */ = {isa = PBXBuildFile; fileRef = BCB613842C68703800485544 /* NodePositionIntent.swift */; }; BCB613872C69A0FB00485544 /* AppIntentErrors.swift in Sources */ = {isa = PBXBuildFile; fileRef = BCB613862C69A0FB00485544 /* AppIntentErrors.swift */; }; + BCD7448D2E0F2FAA00F265A2 /* ContactURLHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = BCD7448C2E0F2FA300F265A2 /* ContactURLHandler.swift */; }; BCD93CBA2D9E11A2006C9214 /* DisconnectNodeIntent.swift in Sources */ = {isa = PBXBuildFile; fileRef = BCD93CB92D9E11A2006C9214 /* DisconnectNodeIntent.swift */; }; BCDDFA9A2DBB180D0065189C /* ScrollToBottomButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = BCDDFA992DBB180D0065189C /* ScrollToBottomButton.swift */; }; BCE2D3C32C7ADF42008E6199 /* ShutDownNodeIntent.swift in Sources */ = {isa = PBXBuildFile; fileRef = BCE2D3C22C7ADF42008E6199 /* ShutDownNodeIntent.swift */; }; @@ -337,6 +338,7 @@ BCB613822C672A2600485544 /* MessageChannelIntent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageChannelIntent.swift; sourceTree = ""; }; BCB613842C68703800485544 /* NodePositionIntent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NodePositionIntent.swift; sourceTree = ""; }; BCB613862C69A0FB00485544 /* AppIntentErrors.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppIntentErrors.swift; sourceTree = ""; }; + BCD7448C2E0F2FA300F265A2 /* ContactURLHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContactURLHandler.swift; sourceTree = ""; }; BCD93CB92D9E11A2006C9214 /* DisconnectNodeIntent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DisconnectNodeIntent.swift; sourceTree = ""; }; BCDDFA992DBB180D0065189C /* ScrollToBottomButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ScrollToBottomButton.swift; sourceTree = ""; }; BCE2D3C22C7ADF42008E6199 /* ShutDownNodeIntent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShutDownNodeIntent.swift; sourceTree = ""; }; @@ -1079,6 +1081,7 @@ DDC2E1A526CEB32B0042C5E4 /* Helpers */ = { isa = PBXGroup; children = ( + BCD7448C2E0F2FA300F265A2 /* ContactURLHandler.swift */, DDD43FE12A78C86B0083A3E9 /* Mqtt */, DDAF8C5226EB1DF10058C060 /* BLEManager.swift */, DD1BEF492E0292220090CE24 /* KeychainHelper.swift */, @@ -1490,6 +1493,7 @@ DDB6ABE628B1406100384BA1 /* LoraConfigEnums.swift in Sources */, DDB8F4142A9EE5F000230ECE /* ChannelList.swift in Sources */, DDD43FE32A78C8900083A3E9 /* MqttClientProxyManager.swift in Sources */, + BCD7448D2E0F2FAA00F265A2 /* ContactURLHandler.swift in Sources */, DD007BB02AA5981000F5FA12 /* NodeInfoEntityExtension.swift in Sources */, DDDB26422AABF655003AFCB7 /* NodeListItem.swift in Sources */, DDDB444629F8A96500EE2349 /* Character.swift in Sources */, diff --git a/Meshtastic/Helpers/BLEManager.swift b/Meshtastic/Helpers/BLEManager.swift index 389e812a..42561a4d 100644 --- a/Meshtastic/Helpers/BLEManager.swift +++ b/Meshtastic/Helpers/BLEManager.swift @@ -1755,7 +1755,7 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate return 0 } - public func saveChannelSet(base64UrlString: String, addChannels: Bool = false) -> Bool { + public func saveChannelSet(base64UrlString: String, addChannels: Bool = false, okToMQTT: Bool = false) -> Bool { if isConnected { var i: Int32 = 0 @@ -1837,6 +1837,7 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate // 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(connectedPeripheral.num) meshPacket.from = UInt32(connectedPeripheral.num) diff --git a/Meshtastic/Helpers/ContactURLHandler.swift b/Meshtastic/Helpers/ContactURLHandler.swift new file mode 100644 index 00000000..749c8cbf --- /dev/null +++ b/Meshtastic/Helpers/ContactURLHandler.swift @@ -0,0 +1,86 @@ +// +// URLHandler.swift +// Meshtastic +// +// Created by Benjamin Faershtein on 6/27/25. +// +import SwiftUI +import CoreData +import OSLog +import TipKit +import MeshtasticProtobufs + +struct ContactURLHandler { + + static var minimumContactVersion = "2.6.9" + + + static func handleContactUrl(url: URL, bleManager: BLEManager) { + let supportedVersion = UserDefaults.firmwareVersion == "0.0.0" || + minimumContactVersion.compare(UserDefaults.firmwareVersion, options: .numeric) == .orderedAscending || + minimumContactVersion.compare(UserDefaults.firmwareVersion, options: .numeric) == .orderedSame + + if !supportedVersion { + let alertController = UIAlertController( + title: "Firmware Upgrade Required", + message: "In order to import contacts via a QR code you need firmware version 2.6.9 or greater.", + preferredStyle: .alert + ) + alertController.addAction(UIAlertAction( + title: "Close", + style: .cancel, + handler: nil + )) + if let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene, + let rootViewController = windowScene.windows.first?.rootViewController { + rootViewController.present(alertController, animated: true) + } + Logger.services.debug("User Alerted that a firmware upgrade is required to import contacts.") + } else { + let components = url.absoluteString.components(separatedBy: "#") + if let contactData = components.last { + let decodedString = contactData.base64urlToBase64() + if let decodedData = Data(base64Encoded: decodedString) { + do { + let contact = try MeshtasticProtobufs.SharedContact(serializedBytes: decodedData) + let alertController = UIAlertController( + title: "Add Contact", + message: "Would you like to add \(contact.user.longName) as a contact?", + preferredStyle: .alert + ) + alertController.addAction(UIAlertAction( + title: "Yes", + style: .default, + handler: { _ in + let success = bleManager.addContactFromURL(base64UrlString: contactData) + Logger.services.debug("Contact added from URL: \(success ? "success" : "failed")") + } + )) + alertController.addAction(UIAlertAction( + title: "No", + style: .cancel, + handler: nil + )) + if let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene, + let rootViewController = windowScene.windows.first?.rootViewController { + rootViewController.present(alertController, animated: true) + } + Logger.services.debug("Contact data extracted from URL: \(contactData, privacy: .public)") + } catch { + Logger.services.error("Failed to parse contact data: \(error.localizedDescription, privacy: .public)") + if let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene, + let rootViewController = windowScene.windows.first?.rootViewController { + let errorAlert = UIAlertController( + title: "Error", + message: "Could not process contact information. Invalid format.", + preferredStyle: .alert + ) + errorAlert.addAction(UIAlertAction(title: "OK", style: .default)) + rootViewController.present(errorAlert, animated: true) + } + } + } + } + } + } +} diff --git a/Meshtastic/MeshtasticApp.swift b/Meshtastic/MeshtasticApp.swift index 1512cae2..b87aeee3 100644 --- a/Meshtastic/MeshtasticApp.swift +++ b/Meshtastic/MeshtasticApp.swift @@ -20,7 +20,6 @@ struct MeshtasticAppleApp: App { @State var incomingUrl: URL? @State var channelSettings: String? @State var addChannels = false - public var minimumContactVersion = "2.6.9" init() { let persistenceController = PersistenceController.shared @@ -44,20 +43,31 @@ struct MeshtasticAppleApp: App { appState: appState, router: appState.router ) - .environment(\.managedObjectContext, persistenceController.container.viewContext) - .environmentObject(appState) - .environmentObject(BLEManager.shared) - .sheet(isPresented: $saveChannels) { - SaveChannelQRCode(channelSetLink: channelSettings ?? "Empty Channel URL", addChannels: addChannels, bleManager: BLEManager.shared) - .presentationDetents([.large]) - .presentationDragIndicator(.visible) + .sheet(isPresented: Binding( + get: { + saveChannels && !(channelSettings == nil) + }, + set: { newValue in + saveChannels = newValue + if !newValue { + channelSettings = nil + } + } + )) { + SaveChannelQRCode( + channelSetLink: channelSettings ?? "Empty Channel URL", + addChannels: addChannels, + bleManager: BLEManager.shared + ) + .presentationDetents([.large]) + .presentationDragIndicator(.visible) } .onContinueUserActivity(NSUserActivityTypeBrowsingWeb) { userActivity in Logger.mesh.debug("URL received \(userActivity, privacy: .public)") self.incomingUrl = userActivity.webpageURL self.saveChannels = false if self.incomingUrl?.absoluteString.lowercased().contains("meshtastic.org/v/#") == true { - handleContactUrl(url: self.incomingUrl!) + ContactURLHandler.handleContactUrl(url: self.incomingUrl!, bleManager: BLEManager.shared) } 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 @@ -74,7 +84,7 @@ struct MeshtasticAppleApp: App { } Logger.services.debug("Add Channel \(self.addChannels, privacy: .public)") } - self.saveChannels = true + self.saveChannels = true Logger.mesh.debug("User wants to open a Channel Settings URL: \(self.incomingUrl?.absoluteString ?? "No QR Code Link")") } if self.saveChannels { @@ -85,7 +95,7 @@ struct MeshtasticAppleApp: App { Logger.mesh.debug("Some sort of URL was received \(url, privacy: .public)") self.incomingUrl = url if url.absoluteString.lowercased().contains("meshtastic.org/v/#") { - handleContactUrl(url: url) + ContactURLHandler.handleContactUrl(url: url, bleManager: BLEManager.shared) } 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 @@ -102,7 +112,7 @@ struct MeshtasticAppleApp: App { } Logger.services.debug("Add Channel \(self.addChannels, privacy: .public)") } - self.saveChannels = true + 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) @@ -141,77 +151,9 @@ struct MeshtasticAppleApp: App { Logger.services.error("🍎 [App] Apple must have changed something") } } + .environment(\.managedObjectContext, persistenceController.container.viewContext) + .environmentObject(appState) + .environmentObject(BLEManager.shared) } - func handleContactUrl(url: URL) { - let supportedVersion = UserDefaults.firmwareVersion == "0.0.0" || self.minimumContactVersion.compare(UserDefaults.firmwareVersion, options: .numeric) == .orderedAscending || minimumContactVersion.compare(UserDefaults.firmwareVersion, options: .numeric) == .orderedSame - if !supportedVersion { - // Show an alert letting the user know they need to upgrade their firmware to use the contact import. - let alertController = UIAlertController( - title: "Firmware Upgrade Required", - message: "In order to import contacts via a QR code you need firmware version 2.6.9 or greater.", - preferredStyle: .alert - ) - alertController.addAction(UIAlertAction( - title: "Close", - style: .cancel, - handler: nil - )) - // Present the alert - if let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene, - let rootViewController = windowScene.windows.first?.rootViewController { - rootViewController.present(alertController, animated: true) - } - Logger.services.debug("User Alerted that a firmware upgrade is required to import contacts.") - } else { - let components = url.absoluteString.components(separatedBy: "#") - // Extract contact information from the URL - if let contactData = components.last { - let decodedString = contactData.base64urlToBase64() - if let decodedData = Data(base64Encoded: decodedString) { - do { - let contact = try MeshtasticProtobufs.SharedContact(serializedBytes: decodedData) - // Show an alert to confirm adding the contact - let alertController = UIAlertController( - title: "Add Contact", - message: "Would you like to add \(contact.user.longName) as a contact?", - preferredStyle: .alert - ) - alertController.addAction(UIAlertAction( - title: "Yes", - style: .default, - handler: { _ in - let success = BLEManager.shared.addContactFromURL(base64UrlString: contactData) - Logger.services.debug("Contact added from URL: \(success ? "success" : "failed")") - } - )) - alertController.addAction(UIAlertAction( - title: "No", - style: .cancel, - handler: nil - )) - // Present the alert - if let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene, - let rootViewController = windowScene.windows.first?.rootViewController { - rootViewController.present(alertController, animated: true) - } - Logger.services.debug("Contact data extracted from URL: \(contactData, privacy: .public)") - } catch { - Logger.services.error("Failed to parse contact data: \(error.localizedDescription, privacy: .public)") - // Show error alert to user - if let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene, - let rootViewController = windowScene.windows.first?.rootViewController { - let errorAlert = UIAlertController( - title: "Error", - message: "Could not process contact information. Invalid format.", - preferredStyle: .alert - ) - errorAlert.addAction(UIAlertAction(title: "OK", style: .default)) - rootViewController.present(errorAlert, animated: true) - } - } - } - } - } - } } diff --git a/Meshtastic/Views/Messages/MessageText.swift b/Meshtastic/Views/Messages/MessageText.swift index c8a994c3..ac033b1f 100644 --- a/Meshtastic/Views/Messages/MessageText.swift +++ b/Meshtastic/Views/Messages/MessageText.swift @@ -17,6 +17,10 @@ struct MessageText: View { let tapBackDestination: MessageDestination 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 isShowingDeleteConfirmation = false @@ -83,6 +87,60 @@ struct MessageText: View { onReply: onReply ) } + .environment(\.openURL, OpenURLAction { url in + channelSettings = nil + + if url.absoluteString.lowercased().contains("meshtastic.org/v/#") { + // Handle contact URL + ContactURLHandler.handleContactUrl(url: url, bleManager: BLEManager.shared) + return .handled // Prevent default browser opening + } else if url.absoluteString.lowercased().contains("meshtastic.org/e/") { + // Handle channel URL + let components = url.absoluteString.components(separatedBy: "#") + guard !components.isEmpty, let lastComponent = components.last else { + Logger.services.error("No valid components found in channel URL: \(url.absoluteString, privacy: .public)") + return .discarded + } + + self.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 + return .discarded + } + + self.channelSettings = lastComponent.components(separatedBy: "?").first ?? "" + + + Logger.services.debug("Add Channel: \(self.addChannels, privacy: .public)") + self.saveChannels = true + 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 + } + } + )) { + SaveChannelQRCode( + channelSetLink: channelSettings ?? "Empty Channel URL", + addChannels: addChannels, + bleManager: BLEManager.shared + ) + .presentationDetents([.large]) + .presentationDragIndicator(.visible) + } .confirmationDialog( "Are you sure you want to delete this message?", isPresented: $isShowingDeleteConfirmation, diff --git a/Meshtastic/Views/Settings/SaveChannelQRCode.swift b/Meshtastic/Views/Settings/SaveChannelQRCode.swift index 892df6eb..49c78b0c 100644 --- a/Meshtastic/Views/Settings/SaveChannelQRCode.swift +++ b/Meshtastic/Views/Settings/SaveChannelQRCode.swift @@ -5,16 +5,24 @@ // Copyright(c) Garth Vander Houwen 7/13/22. // import SwiftUI +import CoreData +import OSLog +import MeshtasticProtobufs struct SaveChannelQRCode: View { - @Environment(\.dismiss) private var dismiss + @Environment(\.managedObjectContext) var context - var channelSetLink: String + let channelSetLink: String var addChannels: Bool = false var bleManager: BLEManager - @State var showError: Bool = false - @State var connectedToDevice = false + + @State private var showError: Bool = false + @State private var errorMessage: String = "" + @State private var connectedToDevice: Bool = false + @State private var loraChanges: [String] = [] + @State private var okToMQTT: Bool = false + var body: some View { VStack { @@ -26,20 +34,50 @@ struct SaveChannelQRCode: View { .font(.title3) .padding() + if !loraChanges.isEmpty { + VStack(alignment: .leading) { + Text("LoRa Config Changes:") + .font(.headline) + .padding(.bottom, 5) + ForEach(loraChanges, id: \.self) { change in + Text("• \(change)") + .font(.callout) + .foregroundColor(.orange) + } + } + .padding() + } + if showError { - Text("Channels being added from the QR code did not save. When adding channels the names must be unique.") + Text(errorMessage.isEmpty ? "Channels being added from the QR code did not save. When adding channels the names must be unique." : errorMessage) .fixedSize(horizontal: false, vertical: true) .foregroundColor(.red) .font(.callout) .padding() } + HStack { if !showError { Button { - let success = bleManager.saveChannelSet(base64UrlString: channelSetLink, addChannels: addChannels) + // Extract channel data if it's a full URL + let channelData: String + if channelSetLink.hasPrefix("http") || channelSetLink.hasPrefix("meshtastic://") { + guard let extractedData = extractChannelDataFromURL(channelSetLink) else { + Logger.data.error("Failed to extract channel data from URL during save: \(channelSetLink)") + errorMessage = "Invalid channel URL format" + showError = true + return + } + channelData = extractedData + } else { + channelData = channelSetLink + } + + let success = bleManager.saveChannelSet(base64UrlString: channelData, addChannels: addChannels, okToMQTT: okToMQTT) if success { dismiss() } else { + errorMessage = "Failed to save channel configuration" showError = true } } label: { @@ -50,24 +88,23 @@ struct SaveChannelQRCode: View { .controlSize(.large) .padding() .disabled(!connectedToDevice) -#if targetEnvironment(macCatalyst) - Button { - dismiss() - } label: { - Label("Cancel", systemImage: "xmark") - } - .buttonStyle(.bordered) - .buttonBorderShape(.capsule) - .controlSize(.large) - .padding() -#endif + #if targetEnvironment(macCatalyst) + Button { + dismiss() + } label: { + Label("Cancel", systemImage: "xmark") + } + .buttonStyle(.bordered) + .buttonBorderShape(.capsule) + .controlSize(.large) + .padding() + #endif } else { Button { dismiss() } label: { Label("Cancel", systemImage: "xmark") - } .buttonStyle(.bordered) .buttonBorderShape(.capsule) @@ -77,7 +114,226 @@ struct SaveChannelQRCode: View { } } .onAppear { + Logger.data.info("Ch set link \(channelSetLink)") connectedToDevice = bleManager.connectToPreferredPeripheral() + fetchLoRaConfigChanges() } } + + private func extractChannelDataFromURL(_ urlString: String) -> String? { + Logger.data.info("Extracting channel data from URL: \(urlString)") + + + if let url = URL(string: urlString) { + // Get the fragment (part after #) + if let fragment = url.fragment, !fragment.isEmpty { + Logger.data.info("Extracted fragment from URL: \(fragment)") + return fragment + } + } + + // Fallback: manually extract everything after the last # + if let hashIndex = urlString.lastIndex(of: "#") { + let startIndex = urlString.index(after: hashIndex) + let channelData = String(urlString[startIndex...]) + if !channelData.isEmpty { + Logger.data.info("Extracted channel data manually: \(channelData)") + return channelData + } + } + + Logger.data.error("Failed to extract channel data from URL: \(urlString)") + return nil + } + + private func fetchLoRaConfigChanges() { + var currentLoRaConfig: Config.LoRaConfig? + + // First, extract the actual channel data from the URL if it's a full URL + let channelData: String + if channelSetLink.hasPrefix("http") || channelSetLink.hasPrefix("meshtastic://") { + guard let extractedData = extractChannelDataFromURL(channelSetLink) else { + Logger.data.error("Failed to extract channel data from URL: \(channelSetLink)") + errorMessage = "Invalid channel URL format" + showError = true + return + } + channelData = extractedData + } else { + // Assume it's already the base64 data + channelData = channelSetLink + } + + Logger.data.info("Processing channel data: \(channelData)") + + // Fetch current LoRa config from Core Data + let fetchRequest = NodeInfoEntity.fetchRequest() + fetchRequest.predicate = NSPredicate(format: "num == %lld", Int64(bleManager.connectedPeripheral?.num ?? 0)) + + do { + let nodes = try context.fetch(fetchRequest) + if let node = nodes.first { + currentLoRaConfig = node.loRaConfig?.toProto() + } + } catch { + Logger.data.error("Failed to fetch NodeInfoEntity: \(error.localizedDescription, privacy: .public)") + } + + // Decode base64url string + let decodedString = channelData.base64urlToBase64() + guard let decodedData = Data(base64Encoded: decodedString) else { + Logger.data.error("Invalid base64 for ChannelSet data: \(channelData, privacy: .public)") + errorMessage = "Invalid channel data format" + showError = true + return + } + + do { + let channelSet = try ChannelSet(serializedBytes: decodedData) + let newLoRaConfig = channelSet.loraConfig + var changes: [String] = [] + + // Preserve user's current okToMQTT setting + okToMQTT = currentLoRaConfig?.configOkToMqtt ?? false + + if let current = currentLoRaConfig { + // Compare each field and track changes + if current.hopLimit != newLoRaConfig.hopLimit { + changes.append("Hop Limit: \(current.hopLimit) -> \(newLoRaConfig.hopLimit)") + } + if current.region != newLoRaConfig.region { + let currentRegionDesc = RegionCodes(rawValue: Int(current.region.rawValue))?.description ?? "Unknown" + let newRegionDesc = RegionCodes(rawValue: Int(newLoRaConfig.region.rawValue))?.description ?? "Unknown" + changes.append("Region: \(currentRegionDesc) -> \(newRegionDesc)") + } + if current.modemPreset != newLoRaConfig.modemPreset { + let currentPresetDesc = ModemPresets(rawValue: Int(current.modemPreset.rawValue))?.description ?? "Unknown" + let newPresetDesc = ModemPresets(rawValue: Int(newLoRaConfig.modemPreset.rawValue))?.description ?? "Unknown" + changes.append("Modem Preset: \(currentPresetDesc) -> \(newPresetDesc)") + } + if current.usePreset != newLoRaConfig.usePreset { + changes.append("Use Preset: \(current.usePreset) -> \(newLoRaConfig.usePreset)") + } + if current.txEnabled != newLoRaConfig.txEnabled { + changes.append("Transmit Enabled: \(current.txEnabled) -> \(newLoRaConfig.txEnabled)") + } + if current.txPower != newLoRaConfig.txPower { + changes.append("Transmit Power: \(current.txPower)dBm -> \(newLoRaConfig.txPower)dBm") + } + if current.channelNum != newLoRaConfig.channelNum { + changes.append("Channel Number: \(current.channelNum) -> \(newLoRaConfig.channelNum)") + } + if current.bandwidth != newLoRaConfig.bandwidth { + changes.append("Bandwidth: \(current.bandwidth) -> \(newLoRaConfig.bandwidth)") + } + if current.codingRate != newLoRaConfig.codingRate { + changes.append("Coding Rate: \(current.codingRate) -> \(newLoRaConfig.codingRate)") + } + if current.spreadFactor != newLoRaConfig.spreadFactor { + changes.append("Spread Factor: \(current.spreadFactor) -> \(newLoRaConfig.spreadFactor)") + } + if current.sx126XRxBoostedGain != newLoRaConfig.sx126XRxBoostedGain { + changes.append("RX Boosted Gain: \(current.sx126XRxBoostedGain) -> \(newLoRaConfig.sx126XRxBoostedGain)") + } + if current.overrideFrequency != newLoRaConfig.overrideFrequency { + changes.append("Override Frequency: \(current.overrideFrequency) -> \(newLoRaConfig.overrideFrequency)") + } + if current.ignoreMqtt != newLoRaConfig.ignoreMqtt { + changes.append("Ignore MQTT: \(current.ignoreMqtt) -> \(newLoRaConfig.ignoreMqtt)") + } + } else { + // Compare against default values when no current config exists + let defaultConfig = getDefaultLoRaConfig() + + if newLoRaConfig.hopLimit != defaultConfig.hopLimit { + changes.append("Hop Limit: \(defaultConfig.hopLimit) -> \(newLoRaConfig.hopLimit)") + } + if newLoRaConfig.region != defaultConfig.region { + let newRegionDesc = RegionCodes(rawValue: Int(newLoRaConfig.region.rawValue))?.description ?? "Unknown" + changes.append("Region: Unset -> \(newRegionDesc)") + } + if newLoRaConfig.modemPreset != defaultConfig.modemPreset { + let newPresetDesc = ModemPresets(rawValue: Int(newLoRaConfig.modemPreset.rawValue))?.description ?? "Unknown" + changes.append("Modem Preset: Long Fast -> \(newPresetDesc)") + } + if newLoRaConfig.usePreset != defaultConfig.usePreset { + changes.append("Use Preset: \(defaultConfig.usePreset) -> \(newLoRaConfig.usePreset)") + } + if newLoRaConfig.txEnabled != defaultConfig.txEnabled { + changes.append("Transmit Enabled: \(defaultConfig.txEnabled) -> \(newLoRaConfig.txEnabled)") + } + if newLoRaConfig.txPower != defaultConfig.txPower { + changes.append("Transmit Power: \(defaultConfig.txPower)dBm -> \(newLoRaConfig.txPower)dBm") + } + if newLoRaConfig.channelNum != defaultConfig.channelNum { + changes.append("Channel Number: \(defaultConfig.channelNum) -> \(newLoRaConfig.channelNum)") + } + if newLoRaConfig.bandwidth != defaultConfig.bandwidth { + changes.append("Bandwidth: \(defaultConfig.bandwidth) -> \(newLoRaConfig.bandwidth)") + } + if newLoRaConfig.codingRate != defaultConfig.codingRate { + changes.append("Coding Rate: \(defaultConfig.codingRate) -> \(newLoRaConfig.codingRate)") + } + if newLoRaConfig.spreadFactor != defaultConfig.spreadFactor { + changes.append("Spread Factor: \(defaultConfig.spreadFactor) -> \(newLoRaConfig.spreadFactor)") + } + if newLoRaConfig.sx126XRxBoostedGain != defaultConfig.sx126XRxBoostedGain { + changes.append("RX Boosted Gain: \(defaultConfig.sx126XRxBoostedGain) -> \(newLoRaConfig.sx126XRxBoostedGain)") + } + if newLoRaConfig.overrideFrequency != defaultConfig.overrideFrequency { + changes.append("Override Frequency: \(defaultConfig.overrideFrequency) -> \(newLoRaConfig.overrideFrequency)") + } + if newLoRaConfig.ignoreMqtt != defaultConfig.ignoreMqtt { + changes.append("Ignore MQTT: \(defaultConfig.ignoreMqtt) -> \(newLoRaConfig.ignoreMqtt)") + } + } + + loraChanges = changes + + } catch { + Logger.data.error("Failed to decode ChannelSet: \(error.localizedDescription, privacy: .public)") + errorMessage = "Failed to decode channel configuration" + showError = true + } + } + + private func getDefaultLoRaConfig() -> Config.LoRaConfig { + var config = Config.LoRaConfig() + config.hopLimit = 3 + config.region = .unset + config.modemPreset = .longFast + config.usePreset = true + config.txEnabled = true + config.txPower = 0 + config.channelNum = 0 + config.bandwidth = 0 + config.codingRate = 0 + config.spreadFactor = 0 + config.sx126XRxBoostedGain = false + config.overrideFrequency = 0.0 + config.ignoreMqtt = false + config.configOkToMqtt = false + return config + } +} + +extension LoRaConfigEntity { + func toProto() -> Config.LoRaConfig { + var config = Config.LoRaConfig() + config.hopLimit = UInt32(self.hopLimit) + config.region = Config.LoRaConfig.RegionCode(rawValue: Int(self.regionCode)) ?? .unset + config.modemPreset = Config.LoRaConfig.ModemPreset(rawValue: Int(self.modemPreset)) ?? .longFast + config.usePreset = self.usePreset + config.txEnabled = self.txEnabled + config.txPower = Int32(self.txPower) + config.channelNum = UInt32(self.channelNum) + config.bandwidth = UInt32(self.bandwidth) + config.codingRate = UInt32(self.codingRate) + config.spreadFactor = UInt32(self.spreadFactor) + config.sx126XRxBoostedGain = self.sx126xRxBoostedGain + config.overrideFrequency = self.overrideFrequency + config.ignoreMqtt = self.ignoreMqtt + config.configOkToMqtt = self.okToMqtt + return config + } } From d7ce318b4d974c2fb9b27c0888d993ff39daebaf Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Sat, 28 Jun 2025 10:11:31 -0700 Subject: [PATCH 182/213] on change for node change --- Localizable.xcstrings | 5 ++++- Meshtastic/Views/Settings/Config/SecurityConfig.swift | 3 +++ 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/Localizable.xcstrings b/Localizable.xcstrings index 0a05a052..cc76d94e 100644 --- a/Localizable.xcstrings +++ b/Localizable.xcstrings @@ -1328,6 +1328,9 @@ } } } + }, + "• %@" : { + }, "< 1%" : { "localizations" : { @@ -35105,4 +35108,4 @@ } }, "version" : "1.0" -} +} \ No newline at end of file diff --git a/Meshtastic/Views/Settings/Config/SecurityConfig.swift b/Meshtastic/Views/Settings/Config/SecurityConfig.swift index 1897bf43..0153bb49 100644 --- a/Meshtastic/Views/Settings/Config/SecurityConfig.swift +++ b/Meshtastic/Views/Settings/Config/SecurityConfig.swift @@ -230,6 +230,9 @@ struct SecurityConfig: View { name: "\(bleManager.connectedPeripheral?.shortName ?? "?")" ) }) + .onChange(of: node) { _, newNode in + setSecurityValues() + } .onChange(of: isManaged) { _, newIsManaged in if newIsManaged != node?.securityConfig?.isManaged { hasChanges = true } } From 202c558c661dc74027203336d0d43e48543ba64c Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Sat, 28 Jun 2025 10:13:24 -0700 Subject: [PATCH 183/213] Dont translate string replacement --- Localizable.xcstrings | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Localizable.xcstrings b/Localizable.xcstrings index cc76d94e..219f123e 100644 --- a/Localizable.xcstrings +++ b/Localizable.xcstrings @@ -1330,7 +1330,7 @@ } }, "• %@" : { - + "shouldTranslate" : false }, "< 1%" : { "localizations" : { From 1778edbad1f676993cd2e903c8a6639fcbdfca54 Mon Sep 17 00:00:00 2001 From: whywilson Date: Mon, 30 Jun 2025 14:20:06 +0800 Subject: [PATCH 184/213] Fix showing canned messages --- Meshtastic/Helpers/MeshPackets.swift | 5 +---- .../Views/Settings/Config/Module/CannedMessagesConfig.swift | 1 + 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/Meshtastic/Helpers/MeshPackets.swift b/Meshtastic/Helpers/MeshPackets.swift index 89d79f60..304865dc 100644 --- a/Meshtastic/Helpers/MeshPackets.swift +++ b/Meshtastic/Helpers/MeshPackets.swift @@ -504,9 +504,6 @@ func adminAppPacket (packet: MeshPacket, context: NSManagedObjectContext) { if adminMessage.payloadVariant == AdminMessage.OneOf_PayloadVariant.getCannedMessageModuleMessagesResponse(adminMessage.getCannedMessageModuleMessagesResponse) { if let cmmc = try? CannedMessageModuleConfig(serializedBytes: packet.decoded.payload) { - - if !cmmc.messages.isEmpty { - let logString = String.localizedStringWithFormat("Canned Messages Messages Received For: %@".localized, packet.from.toHex()) Logger.mesh.info("🥫 \(logString, privacy: .public)") @@ -520,6 +517,7 @@ func adminAppPacket (packet: MeshPacket, context: NSManagedObjectContext) { .replacingOccurrences(of: "11: ", with: "") .replacingOccurrences(of: "\"", with: "") .trimmingCharacters(in: .whitespacesAndNewlines) + .components(separatedBy: "\n")[0] fetchedNode[0].cannedMessageConfig?.messages = messages do { try context.save() @@ -533,7 +531,6 @@ func adminAppPacket (packet: MeshPacket, context: NSManagedObjectContext) { } catch { Logger.data.error("💥 Error Deserializing ADMIN_APP packet.") } - } } } else if adminMessage.payloadVariant == AdminMessage.OneOf_PayloadVariant.getChannelResponse(adminMessage.getChannelResponse) { channelPacket(channel: adminMessage.getChannelResponse, fromNum: Int64(packet.from), context: context) diff --git a/Meshtastic/Views/Settings/Config/Module/CannedMessagesConfig.swift b/Meshtastic/Views/Settings/Config/Module/CannedMessagesConfig.swift index 941ed3fd..28ec19ad 100644 --- a/Meshtastic/Views/Settings/Config/Module/CannedMessagesConfig.swift +++ b/Meshtastic/Views/Settings/Config/Module/CannedMessagesConfig.swift @@ -79,6 +79,7 @@ struct CannedMessagesConfig: View { totalBytes = messages.utf8.count } hasMessagesChanges = true + hasChanges = true } .foregroundColor(.gray) } From 43dd906f86b011778876001808d670a2959ca6f6 Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Mon, 30 Jun 2025 08:58:38 -0700 Subject: [PATCH 185/213] Bump version --- Meshtastic.xcodeproj/project.pbxproj | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/Meshtastic.xcodeproj/project.pbxproj b/Meshtastic.xcodeproj/project.pbxproj index 84c37c28..d11194bd 100644 --- a/Meshtastic.xcodeproj/project.pbxproj +++ b/Meshtastic.xcodeproj/project.pbxproj @@ -1823,12 +1823,12 @@ INFOPLIST_FILE = Meshtastic/Info.plist; INFOPLIST_KEY_CFBundleDisplayName = Meshtastic; INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.utilities"; - IPHONEOS_DEPLOYMENT_TARGET = 17.3; + IPHONEOS_DEPLOYMENT_TARGET = 17.5; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 2.6.10; + MARKETING_VERSION = 2.6.11; PRODUCT_BUNDLE_IDENTIFIER = gvh.MeshtasticClient; PRODUCT_NAME = "$(TARGET_NAME)"; SUPPORTS_MACCATALYST = YES; @@ -1856,12 +1856,12 @@ INFOPLIST_FILE = Meshtastic/Info.plist; INFOPLIST_KEY_CFBundleDisplayName = Meshtastic; INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.utilities"; - IPHONEOS_DEPLOYMENT_TARGET = 17.3; + IPHONEOS_DEPLOYMENT_TARGET = 17.5; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 2.6.10; + MARKETING_VERSION = 2.6.11; PRODUCT_BUNDLE_IDENTIFIER = gvh.MeshtasticClient; PRODUCT_NAME = "$(TARGET_NAME)"; SUPPORTS_MACCATALYST = YES; @@ -1886,13 +1886,13 @@ INFOPLIST_FILE = Widgets/Info.plist; INFOPLIST_KEY_CFBundleDisplayName = Widgets; INFOPLIST_KEY_NSHumanReadableCopyright = ""; - IPHONEOS_DEPLOYMENT_TARGET = 17.3; + IPHONEOS_DEPLOYMENT_TARGET = 17.5; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", "@executable_path/../../Frameworks", ); - MARKETING_VERSION = 2.6.10; + MARKETING_VERSION = 2.6.11; PRODUCT_BUNDLE_IDENTIFIER = gvh.MeshtasticClient.Widgets; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; @@ -1918,13 +1918,13 @@ INFOPLIST_FILE = Widgets/Info.plist; INFOPLIST_KEY_CFBundleDisplayName = Widgets; INFOPLIST_KEY_NSHumanReadableCopyright = ""; - IPHONEOS_DEPLOYMENT_TARGET = 17.3; + IPHONEOS_DEPLOYMENT_TARGET = 17.5; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", "@executable_path/../../Frameworks", ); - MARKETING_VERSION = 2.6.10; + MARKETING_VERSION = 2.6.11; PRODUCT_BUNDLE_IDENTIFIER = gvh.MeshtasticClient.Widgets; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; From 648a82ff6a1f2e850c06d501028c1a2627e67c30 Mon Sep 17 00:00:00 2001 From: Jake-B Date: Wed, 2 Jul 2025 09:20:35 -0400 Subject: [PATCH 186/213] fixed a typo in a comment --- Meshtastic/Helpers/MeshPackets.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Meshtastic/Helpers/MeshPackets.swift b/Meshtastic/Helpers/MeshPackets.swift index 89d79f60..a6220792 100644 --- a/Meshtastic/Helpers/MeshPackets.swift +++ b/Meshtastic/Helpers/MeshPackets.swift @@ -15,7 +15,7 @@ import OSLog import ActivityKit #endif -// Simple extension to consicely pass values through a has_XXX boolean check +// Simple extension to concisely pass values through a has_XXX boolean check fileprivate extension Bool { func then(_ value: T) -> T? { self ? value : nil From 8af69fe57b76ab50a6439315b311f8faa0d41e89 Mon Sep 17 00:00:00 2001 From: Jake-B Date: Wed, 2 Jul 2025 09:31:56 -0400 Subject: [PATCH 187/213] Moved context access to @Environment --- .../Nodes/Helpers/Map/WaypointForm.swift | 27 ++++++++++--------- 1 file changed, 14 insertions(+), 13 deletions(-) diff --git a/Meshtastic/Views/Nodes/Helpers/Map/WaypointForm.swift b/Meshtastic/Views/Nodes/Helpers/Map/WaypointForm.swift index 5866e8ed..2f74d51f 100644 --- a/Meshtastic/Views/Nodes/Helpers/Map/WaypointForm.swift +++ b/Meshtastic/Views/Nodes/Helpers/Map/WaypointForm.swift @@ -14,6 +14,7 @@ import SwiftUI struct WaypointForm: View { @EnvironmentObject var bleManager: BLEManager + @Environment(\.managedObjectContext) var context @Environment(\.dismiss) private var dismiss @State var waypoint: WaypointEntity let distanceFormatter = MKDistanceFormatter() @@ -210,11 +211,11 @@ struct WaypointForm: View { Menu { Button("For me", action: { - bleManager.context.delete(waypoint) + context.delete(waypoint) do { - try bleManager.context.save() + try context.save() } catch { - bleManager.context.rollback() + context.rollback() } dismiss() }) Button("For everyone", action: { @@ -239,11 +240,11 @@ struct WaypointForm: View { newWaypoint.expire = UInt32(1) if bleManager.sendWaypoint(waypoint: newWaypoint) { - bleManager.context.delete(waypoint) + context.delete(waypoint) do { - try bleManager.context.save() + try context.save() } catch { - bleManager.context.rollback() + context.rollback() } dismiss() } else { @@ -384,11 +385,11 @@ struct WaypointForm: View { } .alert("Waypoint Failed to Send", isPresented: $waypointFailedAlert) { Button("OK", role: .cancel) { - bleManager.context.delete(waypoint) + context.delete(waypoint) do { - try bleManager.context.save() + try context.save() } catch { - bleManager.context.rollback() + context.rollback() } dismiss() } @@ -396,18 +397,18 @@ struct WaypointForm: View { .onDisappear { if waypoint.id == 0 { // New, unsent waypoint created by the user: delete it - bleManager.context.delete(waypoint) + context.delete(waypoint) do { - try bleManager.context.save() + try context.save() } catch { - bleManager.context.rollback() + context.rollback() Logger.mesh.error("Failed to save context on waypoint deletion: \(error)") } } } .onAppear { if waypoint.id > 0 { - let waypoint = getWaypoint(id: Int64(waypoint.id), context: bleManager.context) + let waypoint = getWaypoint(id: Int64(waypoint.id), context: context) name = waypoint.name ?? "Dropped Pin" description = waypoint.longDescription ?? "" icon = String(UnicodeScalar(Int(waypoint.icon)) ?? "📍") From 12c8cd95663c9d8926721063f7f00569c2badabc Mon Sep 17 00:00:00 2001 From: Ben Meadors Date: Thu, 3 Jul 2025 19:32:56 -0500 Subject: [PATCH 188/213] Datadog monitoring --- Meshtastic.xcodeproj/project.pbxproj | 41 +++++++++++++++++++ .../xcshareddata/swiftpm/Package.resolved | 29 ++++++++++++- Meshtastic/MeshtasticApp.swift | 25 +++++++++++ MeshtasticProtobufs/Package.resolved | 15 +++++++ 4 files changed, 109 insertions(+), 1 deletion(-) create mode 100644 MeshtasticProtobufs/Package.resolved diff --git a/Meshtastic.xcodeproj/project.pbxproj b/Meshtastic.xcodeproj/project.pbxproj index d11194bd..4b50b09f 100644 --- a/Meshtastic.xcodeproj/project.pbxproj +++ b/Meshtastic.xcodeproj/project.pbxproj @@ -7,6 +7,10 @@ objects = { /* Begin PBXBuildFile section */ + 102B5EAB2E172F41003D191E /* DatadogCore in Frameworks */ = {isa = PBXBuildFile; productRef = 102B5EAA2E172F41003D191E /* DatadogCore */; }; + 102B5EAD2E172F41003D191E /* DatadogCrashReporting in Frameworks */ = {isa = PBXBuildFile; productRef = 102B5EAC2E172F41003D191E /* DatadogCrashReporting */; }; + 102B5EAF2E172F41003D191E /* DatadogLogs in Frameworks */ = {isa = PBXBuildFile; productRef = 102B5EAE2E172F41003D191E /* DatadogLogs */; }; + 102B5EB12E172F41003D191E /* DatadogRUM in Frameworks */ = {isa = PBXBuildFile; productRef = 102B5EB02E172F41003D191E /* DatadogRUM */; }; 108FFECB2DD3F43C00BFAA81 /* ShareContactQRDialog.swift in Sources */ = {isa = PBXBuildFile; fileRef = 108FFECA2DD3F43C00BFAA81 /* ShareContactQRDialog.swift */; }; 108FFECD2DD4005600BFAA81 /* NodeInfoEntityToNodeInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 108FFECC2DD4005600BFAA81 /* NodeInfoEntityToNodeInfo.swift */; }; 231B3F212D087A4C0069A07D /* MetricTableColumn.swift in Sources */ = {isa = PBXBuildFile; fileRef = 231B3F202D087A4C0069A07D /* MetricTableColumn.swift */; }; @@ -592,7 +596,11 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( + 102B5EAD2E172F41003D191E /* DatadogCrashReporting in Frameworks */, 25A978BA2C13F8ED0003AAE7 /* MeshtasticProtobufs in Frameworks */, + 102B5EAB2E172F41003D191E /* DatadogCore in Frameworks */, + 102B5EAF2E172F41003D191E /* DatadogLogs in Frameworks */, + 102B5EB12E172F41003D191E /* DatadogRUM in Frameworks */, DD0D3D222A55CEB10066DB71 /* CocoaMQTT in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; @@ -1236,6 +1244,10 @@ packageProductDependencies = ( DD0D3D212A55CEB10066DB71 /* CocoaMQTT */, 25A978B92C13F8ED0003AAE7 /* MeshtasticProtobufs */, + 102B5EAA2E172F41003D191E /* DatadogCore */, + 102B5EAC2E172F41003D191E /* DatadogCrashReporting */, + 102B5EAE2E172F41003D191E /* DatadogLogs */, + 102B5EB02E172F41003D191E /* DatadogRUM */, ); productName = MeshtasticClient; productReference = DDC2E15426CE248E0042C5E4 /* Meshtastic.app */; @@ -1305,6 +1317,7 @@ DD0D3D202A55CEB10066DB71 /* XCRemoteSwiftPackageReference "CocoaMQTT" */, 25A978B82C13F8ED0003AAE7 /* XCLocalSwiftPackageReference "MeshtasticProtobufs" */, 259792242C2F10B600AD1659 /* XCRemoteSwiftPackageReference "swift-protobuf" */, + 102B5EA92E172F41003D191E /* XCRemoteSwiftPackageReference "dd-sdk-ios" */, ); productRefGroup = DDC2E15526CE248E0042C5E4 /* Products */; projectDirPath = ""; @@ -1985,6 +1998,14 @@ /* End XCLocalSwiftPackageReference section */ /* Begin XCRemoteSwiftPackageReference section */ + 102B5EA92E172F41003D191E /* XCRemoteSwiftPackageReference "dd-sdk-ios" */ = { + isa = XCRemoteSwiftPackageReference; + repositoryURL = "https://github.com/DataDog/dd-sdk-ios.git"; + requirement = { + kind = upToNextMajorVersion; + minimumVersion = 2.29.0; + }; + }; 259792242C2F10B600AD1659 /* XCRemoteSwiftPackageReference "swift-protobuf" */ = { isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/apple/swift-protobuf.git"; @@ -2004,6 +2025,26 @@ /* End XCRemoteSwiftPackageReference section */ /* Begin XCSwiftPackageProductDependency section */ + 102B5EAA2E172F41003D191E /* DatadogCore */ = { + isa = XCSwiftPackageProductDependency; + package = 102B5EA92E172F41003D191E /* XCRemoteSwiftPackageReference "dd-sdk-ios" */; + productName = DatadogCore; + }; + 102B5EAC2E172F41003D191E /* DatadogCrashReporting */ = { + isa = XCSwiftPackageProductDependency; + package = 102B5EA92E172F41003D191E /* XCRemoteSwiftPackageReference "dd-sdk-ios" */; + productName = DatadogCrashReporting; + }; + 102B5EAE2E172F41003D191E /* DatadogLogs */ = { + isa = XCSwiftPackageProductDependency; + package = 102B5EA92E172F41003D191E /* XCRemoteSwiftPackageReference "dd-sdk-ios" */; + productName = DatadogLogs; + }; + 102B5EB02E172F41003D191E /* DatadogRUM */ = { + isa = XCSwiftPackageProductDependency; + package = 102B5EA92E172F41003D191E /* XCRemoteSwiftPackageReference "dd-sdk-ios" */; + productName = DatadogRUM; + }; 25A978B92C13F8ED0003AAE7 /* MeshtasticProtobufs */ = { isa = XCSwiftPackageProductDependency; productName = MeshtasticProtobufs; diff --git a/Meshtastic.xcworkspace/xcshareddata/swiftpm/Package.resolved b/Meshtastic.xcworkspace/xcshareddata/swiftpm/Package.resolved index 8cb1b6ba..68276229 100644 --- a/Meshtastic.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/Meshtastic.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -1,5 +1,5 @@ { - "originHash" : "a3033aea781828906c453276e3723177901ce64df5757de7ada28c854c9662eb", + "originHash" : "0dabe052e9e56f8514254d01df9aa7245e16b28a649d59bac6781d4ac9a79efa", "pins" : [ { "identity" : "cocoamqtt", @@ -10,6 +10,15 @@ "version" : "2.1.8" } }, + { + "identity" : "dd-sdk-ios", + "kind" : "remoteSourceControl", + "location" : "https://github.com/DataDog/dd-sdk-ios.git", + "state" : { + "revision" : "d0a42d8067665cb6ee86af51251ccc071f62bd54", + "version" : "2.29.0" + } + }, { "identity" : "mqttcocoaasyncsocket", "kind" : "remoteSourceControl", @@ -19,6 +28,24 @@ "version" : "1.0.8" } }, + { + "identity" : "opentelemetry-swift-packages", + "kind" : "remoteSourceControl", + "location" : "https://github.com/DataDog/opentelemetry-swift-packages.git", + "state" : { + "revision" : "4a7295600d4ebb9525a23c11586c5fdb74ae8b7e", + "version" : "1.13.1" + } + }, + { + "identity" : "plcrashreporter", + "kind" : "remoteSourceControl", + "location" : "https://github.com/microsoft/plcrashreporter.git", + "state" : { + "revision" : "8c61e5e38e9f737dd68512ed1ea5ab081244ad65", + "version" : "1.12.0" + } + }, { "identity" : "starscream", "kind" : "remoteSourceControl", diff --git a/Meshtastic/MeshtasticApp.swift b/Meshtastic/MeshtasticApp.swift index b87aeee3..22ea61f3 100644 --- a/Meshtastic/MeshtasticApp.swift +++ b/Meshtastic/MeshtasticApp.swift @@ -5,6 +5,9 @@ import CoreData import OSLog import TipKit import MeshtasticProtobufs +import DatadogCore +import DatadogCrashReporting +import DatadogRUM @main struct MeshtasticAppleApp: App { @@ -26,6 +29,28 @@ struct MeshtasticAppleApp: App { let appState = AppState( router: Router() ) + // Initialize Datadog + // RUM Client Tokens are NOT secret + let appID = "79fe92a9-74c9-4c8f-ba63-6308384ecfa9" + let clientToken = "pub4427bea20dbdb08a6af68034de22cd3b" + let environment = "testflight" + + Datadog.initialize( + with: Datadog.Configuration( + clientToken: clientToken, + env: environment, + site: .us5 + ), + trackingConsent: .granted + ) + + RUM.enable( + with: RUM.Configuration( + applicationID: appID, + uiKitViewsPredicate: DefaultUIKitRUMViewsPredicate(), + uiKitActionsPredicate: DefaultUIKitRUMActionsPredicate() + ) + ) self._appState = ObservedObject(wrappedValue: appState) // Initialize the BLEManager singleton with the necessary dependencies BLEManager.setup(appState: appState, context: persistenceController.container.viewContext) diff --git a/MeshtasticProtobufs/Package.resolved b/MeshtasticProtobufs/Package.resolved new file mode 100644 index 00000000..a679a95e --- /dev/null +++ b/MeshtasticProtobufs/Package.resolved @@ -0,0 +1,15 @@ +{ + "originHash" : "a2385deee281bd55bce80722a1f2b020f7b745c02005befa8ccbf58a39ef4002", + "pins" : [ + { + "identity" : "swift-protobuf", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-protobuf.git", + "state" : { + "revision" : "d72aed98f8253ec1aa9ea1141e28150f408cf17f", + "version" : "1.29.0" + } + } + ], + "version" : 3 +} From b877aafc3f5fe8df7cd84a6421da16b9e6b475dd Mon Sep 17 00:00:00 2001 From: Mathew Kamkar <578302+matkam@users.noreply.github.com> Date: Sat, 5 Jul 2025 08:22:24 -0700 Subject: [PATCH 189/213] only hide unmonitored nodes when no messages --- Meshtastic/Views/Messages/UserList.swift | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Meshtastic/Views/Messages/UserList.swift b/Meshtastic/Views/Messages/UserList.swift index 41642582..bd93b495 100644 --- a/Meshtastic/Views/Messages/UserList.swift +++ b/Meshtastic/Views/Messages/UserList.swift @@ -337,7 +337,8 @@ struct FilteredUserList: View { } } // Always apply unmessagable and connected node filters - let isUnmessagablePredicate = NSPredicate(format: "unmessagable == NO") + // Only hide unmessagable nodes if they have 0 messages + let isUnmessagablePredicate = NSPredicate(format: "unmessagable == NO AND (SUBQUERY(messageList, $msg, $msg.messageId != nil).@count == 0)") predicates.append(isUnmessagablePredicate) let isIgnoredPredicate = NSPredicate(format: "userNode.ignored == NO") predicates.append(isIgnoredPredicate) From 1e6cbcf06f2c19c7fa8a23f13b7329ecc500b3b2 Mon Sep 17 00:00:00 2001 From: Mathew Kamkar <578302+matkam@users.noreply.github.com> Date: Sat, 5 Jul 2025 10:49:10 -0700 Subject: [PATCH 190/213] fix crash --- Meshtastic/Views/Messages/UserList.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Meshtastic/Views/Messages/UserList.swift b/Meshtastic/Views/Messages/UserList.swift index bd93b495..0798ffbf 100644 --- a/Meshtastic/Views/Messages/UserList.swift +++ b/Meshtastic/Views/Messages/UserList.swift @@ -337,8 +337,8 @@ struct FilteredUserList: View { } } // Always apply unmessagable and connected node filters - // Only hide unmessagable nodes if they have 0 messages - let isUnmessagablePredicate = NSPredicate(format: "unmessagable == NO AND (SUBQUERY(messageList, $msg, $msg.messageId != nil).@count == 0)") + // Show unmessagable nodes only if they have messages, otherwise hide them + let isUnmessagablePredicate = NSPredicate(format: "unmessagable == NO OR ((SUBQUERY(receivedMessages, $msg, $msg.messageId != nil).@count > 0) OR (SUBQUERY(sentMessages, $msg, $msg.messageId != nil).@count > 0))") predicates.append(isUnmessagablePredicate) let isIgnoredPredicate = NSPredicate(format: "userNode.ignored == NO") predicates.append(isIgnoredPredicate) From b85a0336edc4ffa658eb596bd388e1ce182e93f5 Mon Sep 17 00:00:00 2001 From: Ben Meadors Date: Mon, 7 Jul 2025 09:57:27 -0500 Subject: [PATCH 191/213] Less forced unwrapping of connectedPeripherals to resolve crashes --- Meshtastic/Helpers/BLEManager.swift | 26 +++++++++++++++----------- 1 file changed, 15 insertions(+), 11 deletions(-) diff --git a/Meshtastic/Helpers/BLEManager.swift b/Meshtastic/Helpers/BLEManager.swift index 42561a4d..78a58ce9 100644 --- a/Meshtastic/Helpers/BLEManager.swift +++ b/Meshtastic/Helpers/BLEManager.swift @@ -499,7 +499,7 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate let traceRoute = TraceRouteEntity(context: context) let nodes = NodeInfoEntity.fetchRequest() - nodes.predicate = NSPredicate(format: "num IN %@", [destNum, self.connectedPeripheral.num]) + nodes.predicate = NSPredicate(format: "num IN %@", [destNum, self.connectedPeripheral?.num ?? 0]) do { let fetchedNodes = try context.fetch(nodes) let receivingNode = fetchedNodes.first(where: { $0.num == destNum }) @@ -803,16 +803,16 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate // Config if decodedInfo.config.isInitialized && !invalidVersion && connectedPeripheral != nil { nowKnown = true - localConfig(config: decodedInfo.config, context: context, nodeNum: Int64(truncatingIfNeeded: self.connectedPeripheral.num), nodeLongName: self.connectedPeripheral.longName) + localConfig(config: decodedInfo.config, context: context, nodeNum: Int64(truncatingIfNeeded: self.connectedPeripheral?.num ?? 0), nodeLongName: self.connectedPeripheral?.longName ?? "Unknown") } // Module Config if decodedInfo.moduleConfig.isInitialized && !invalidVersion && self.connectedPeripheral?.num != 0 { onWantConfigResponseReceived() nowKnown = true - moduleConfig(config: decodedInfo.moduleConfig, context: context, nodeNum: Int64(truncatingIfNeeded: self.connectedPeripheral?.num ?? 0), nodeLongName: self.connectedPeripheral.longName) + moduleConfig(config: decodedInfo.moduleConfig, context: context, nodeNum: Int64(truncatingIfNeeded: self.connectedPeripheral?.num ?? 0), nodeLongName: self.connectedPeripheral?.longName ?? "Unknown") if decodedInfo.moduleConfig.payloadVariant == ModuleConfig.OneOf_PayloadVariant.cannedMessage(decodedInfo.moduleConfig.cannedMessage) { if decodedInfo.moduleConfig.cannedMessage.enabled { - _ = self.getCannedMessageModuleMessages(destNum: self.connectedPeripheral.num, wantResponse: true) + _ = self.getCannedMessageModuleMessages(destNum: self.connectedPeripheral?.num ?? 0, wantResponse: true) } } } @@ -866,7 +866,7 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate case .nodeinfoApp: if !invalidVersion { upsertNodeInfoPacket(packet: decodedInfo.packet, context: context) } case .routingApp: - if !invalidVersion { routingPacket(packet: decodedInfo.packet, connectedNodeNum: self.connectedPeripheral.num, context: context) } + if !invalidVersion { routingPacket(packet: decodedInfo.packet, connectedNodeNum: self.connectedPeripheral?.num ?? 0, context: context) } case .adminApp: adminAppPacket(packet: decodedInfo.packet, context: context) case .replyApp: @@ -1174,7 +1174,7 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate success = false } else { - let fromUserNum: Int64 = self.connectedPeripheral.num + let fromUserNum: Int64 = self.connectedPeripheral?.num ?? 0 let messageUsers = UserEntity.fetchRequest() messageUsers.predicate = NSPredicate(format: "num IN %@", [fromUserNum, Int64(toUserNum)]) @@ -1230,7 +1230,7 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate newMessage.toUser?.userNode?.favorite = true do { try context.save() - Logger.data.info("💾 Auto favorited node bases on sending a message \(self.connectedPeripheral.num.toHex(), privacy: .public) to \(toUserNum.toHex(), privacy: .public)") + Logger.data.info("💾 Auto favorited node bases on sending a message \(self.connectedPeripheral?.num.toHex() ?? "0", privacy: .public) to \(toUserNum.toHex(), privacy: .public)") _ = self.setFavoriteNode(node: (newMessage.toUser?.userNode)!, connectedNodeNum: fromUserNum) } catch { context.rollback() @@ -1267,7 +1267,7 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate Logger.mesh.info("💬 \(logString, privacy: .public)") do { try context.save() - Logger.data.info("💾 Saved a new sent message from \(self.connectedPeripheral.num.toHex(), privacy: .public) to \(toUserNum.toHex(), privacy: .public)") + Logger.data.info("💾 Saved a new sent message from \(self.connectedPeripheral?.num.toHex() ?? "0", privacy: .public) to \(toUserNum.toHex(), privacy: .public)") success = true } catch { @@ -1278,7 +1278,7 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate } } } catch { - Logger.data.error("💥 Send message failure \(self.connectedPeripheral.num.toHex(), privacy: .public) to \(toUserNum.toHex(), privacy: .public)") + Logger.data.error("💥 Send message failure \(self.connectedPeripheral?.num.toHex() ?? "0", privacy: .public) to \(toUserNum.toHex(), privacy: .public)") } } return success @@ -1495,11 +1495,15 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate } public func sendTime() -> Bool { + if self.connectedPeripheral?.num ?? 0 <= 0 { + Logger.mesh.error("🚫 Unable to send time, connected node is disconnected or invalid") + return false + } var adminPacket = AdminMessage() adminPacket.setTimeOnly = UInt32(Date().timeIntervalSince1970) var meshPacket: MeshPacket = MeshPacket() - meshPacket.to = UInt32(self.connectedPeripheral.num) - meshPacket.from = UInt32(self.connectedPeripheral.num) + meshPacket.to = UInt32(self.connectedPeripheral?.num ?? 0) + meshPacket.from = UInt32(self.connectedPeripheral?.num ?? 0) meshPacket.id = UInt32.random(in: UInt32(UInt8.max).. Date: Mon, 7 Jul 2025 10:46:57 -0500 Subject: [PATCH 192/213] Update Meshtastic/Helpers/BLEManager.swift Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- Meshtastic/Helpers/BLEManager.swift | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/Meshtastic/Helpers/BLEManager.swift b/Meshtastic/Helpers/BLEManager.swift index 78a58ce9..2cfc90dd 100644 --- a/Meshtastic/Helpers/BLEManager.swift +++ b/Meshtastic/Helpers/BLEManager.swift @@ -812,7 +812,9 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate moduleConfig(config: decodedInfo.moduleConfig, context: context, nodeNum: Int64(truncatingIfNeeded: self.connectedPeripheral?.num ?? 0), nodeLongName: self.connectedPeripheral?.longName ?? "Unknown") if decodedInfo.moduleConfig.payloadVariant == ModuleConfig.OneOf_PayloadVariant.cannedMessage(decodedInfo.moduleConfig.cannedMessage) { if decodedInfo.moduleConfig.cannedMessage.enabled { - _ = self.getCannedMessageModuleMessages(destNum: self.connectedPeripheral?.num ?? 0, wantResponse: true) + if let validNum = self.connectedPeripheral?.num, validNum > 0 { + _ = self.getCannedMessageModuleMessages(destNum: validNum, wantResponse: true) + } } } } From 30d150e3aa14765f623abb3fa269119e53c3224f Mon Sep 17 00:00:00 2001 From: Ben Meadors Date: Mon, 7 Jul 2025 10:49:52 -0500 Subject: [PATCH 193/213] Apply suggestion from @Copilot Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- Meshtastic/Helpers/BLEManager.swift | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/Meshtastic/Helpers/BLEManager.swift b/Meshtastic/Helpers/BLEManager.swift index 2cfc90dd..e7cc671a 100644 --- a/Meshtastic/Helpers/BLEManager.swift +++ b/Meshtastic/Helpers/BLEManager.swift @@ -499,7 +499,11 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate let traceRoute = TraceRouteEntity(context: context) let nodes = NodeInfoEntity.fetchRequest() - nodes.predicate = NSPredicate(format: "num IN %@", [destNum, self.connectedPeripheral?.num ?? 0]) + if let connectedNum = self.connectedPeripheral?.num { + nodes.predicate = NSPredicate(format: "num IN %@", [destNum, connectedNum]) + } else { + nodes.predicate = NSPredicate(format: "num == %@", destNum) + } do { let fetchedNodes = try context.fetch(nodes) let receivingNode = fetchedNodes.first(where: { $0.num == destNum }) From 6b262aa44837766c4bed2799ea1e01095e42635d Mon Sep 17 00:00:00 2001 From: Ben Meadors Date: Mon, 7 Jul 2025 10:50:18 -0500 Subject: [PATCH 194/213] Apply suggestion from @Copilot Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- Meshtastic/Helpers/BLEManager.swift | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/Meshtastic/Helpers/BLEManager.swift b/Meshtastic/Helpers/BLEManager.swift index e7cc671a..96268569 100644 --- a/Meshtastic/Helpers/BLEManager.swift +++ b/Meshtastic/Helpers/BLEManager.swift @@ -872,7 +872,13 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate case .nodeinfoApp: if !invalidVersion { upsertNodeInfoPacket(packet: decodedInfo.packet, context: context) } case .routingApp: - if !invalidVersion { routingPacket(packet: decodedInfo.packet, connectedNodeNum: self.connectedPeripheral?.num ?? 0, context: context) } + if !invalidVersion { + guard let connectedPeripheral = self.connectedPeripheral else { + Logger.mesh.error("🕸️ connectedPeripheral is nil. Unable to determine connectedNodeNum for routingPacket.") + return + } + routingPacket(packet: decodedInfo.packet, connectedNodeNum: connectedPeripheral.num, context: context) + } case .adminApp: adminAppPacket(packet: decodedInfo.packet, context: context) case .replyApp: From c8c79abb9e3d678a6f6d1744ac3ea0f60785f83c Mon Sep 17 00:00:00 2001 From: Ben Meadors Date: Mon, 7 Jul 2025 10:50:30 -0500 Subject: [PATCH 195/213] Apply suggestion from @Copilot Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- Meshtastic/Helpers/BLEManager.swift | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/Meshtastic/Helpers/BLEManager.swift b/Meshtastic/Helpers/BLEManager.swift index 96268569..b5d12ae9 100644 --- a/Meshtastic/Helpers/BLEManager.swift +++ b/Meshtastic/Helpers/BLEManager.swift @@ -1186,7 +1186,10 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate success = false } else { - let fromUserNum: Int64 = self.connectedPeripheral?.num ?? 0 + guard let fromUserNum = self.connectedPeripheral?.num else { + Logger.mesh.error("🚫 Connected peripheral user number is nil, cannot send message.") + return false + } let messageUsers = UserEntity.fetchRequest() messageUsers.predicate = NSPredicate(format: "num IN %@", [fromUserNum, Int64(toUserNum)]) From 9f4653ab53245a58f53fe888afe5efd5934d4778 Mon Sep 17 00:00:00 2001 From: Ben Meadors Date: Mon, 7 Jul 2025 10:50:45 -0500 Subject: [PATCH 196/213] Apply suggestion from @Copilot Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- Meshtastic/Helpers/BLEManager.swift | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/Meshtastic/Helpers/BLEManager.swift b/Meshtastic/Helpers/BLEManager.swift index b5d12ae9..ab1c8cf9 100644 --- a/Meshtastic/Helpers/BLEManager.swift +++ b/Meshtastic/Helpers/BLEManager.swift @@ -1245,7 +1245,11 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate newMessage.toUser?.userNode?.favorite = true do { try context.save() - Logger.data.info("💾 Auto favorited node bases on sending a message \(self.connectedPeripheral?.num.toHex() ?? "0", privacy: .public) to \(toUserNum.toHex(), privacy: .public)") + if let connectedPeripheral = self.connectedPeripheral { + Logger.data.info("💾 Auto favorited node based on sending a message \(connectedPeripheral.num.toHex(), privacy: .public) to \(toUserNum.toHex(), privacy: .public)") + } else { + Logger.data.warning("⚠️ connectedPeripheral is nil while attempting to log auto-favoriting a node.") + } _ = self.setFavoriteNode(node: (newMessage.toUser?.userNode)!, connectedNodeNum: fromUserNum) } catch { context.rollback() From ac61ce4b60eaf7db8825ee2b1236b7fff257f90e Mon Sep 17 00:00:00 2001 From: Ben Meadors Date: Mon, 7 Jul 2025 11:55:43 -0500 Subject: [PATCH 197/213] Prefer guards --- Meshtastic/Helpers/BLEManager.swift | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Meshtastic/Helpers/BLEManager.swift b/Meshtastic/Helpers/BLEManager.swift index ab1c8cf9..eded69b0 100644 --- a/Meshtastic/Helpers/BLEManager.swift +++ b/Meshtastic/Helpers/BLEManager.swift @@ -805,15 +805,15 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate channelPacket(channel: decodedInfo.channel, fromNum: Int64(truncatingIfNeeded: connectedPeripheral.num), context: context) } // Config - if decodedInfo.config.isInitialized && !invalidVersion && connectedPeripheral != nil { + if decodedInfo.config.isInitialized && !invalidVersion && connectedPeripheral != nil && self.connectedPeripheral?.num > 0 { nowKnown = true - localConfig(config: decodedInfo.config, context: context, nodeNum: Int64(truncatingIfNeeded: self.connectedPeripheral?.num ?? 0), nodeLongName: self.connectedPeripheral?.longName ?? "Unknown") + localConfig(config: decodedInfo.config, context: context, nodeNum: Int64(truncatingIfNeeded: self.connectedPeripheral.num), nodeLongName: self.connectedPeripheral?.longName ?? "Unknown") } // Module Config if decodedInfo.moduleConfig.isInitialized && !invalidVersion && self.connectedPeripheral?.num != 0 { onWantConfigResponseReceived() nowKnown = true - moduleConfig(config: decodedInfo.moduleConfig, context: context, nodeNum: Int64(truncatingIfNeeded: self.connectedPeripheral?.num ?? 0), nodeLongName: self.connectedPeripheral?.longName ?? "Unknown") + moduleConfig(config: decodedInfo.moduleConfig, context: context, nodeNum: Int64(truncatingIfNeeded: self.connectedPeripheral?.num), nodeLongName: self.connectedPeripheral?.longName ?? "Unknown") if decodedInfo.moduleConfig.payloadVariant == ModuleConfig.OneOf_PayloadVariant.cannedMessage(decodedInfo.moduleConfig.cannedMessage) { if decodedInfo.moduleConfig.cannedMessage.enabled { if let validNum = self.connectedPeripheral?.num, validNum > 0 { From b03ef7fa17b2d50572c95a64a8c9e008705c7a79 Mon Sep 17 00:00:00 2001 From: Ben Meadors Date: Mon, 7 Jul 2025 11:59:21 -0500 Subject: [PATCH 198/213] Comparison --- Meshtastic/Helpers/BLEManager.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Meshtastic/Helpers/BLEManager.swift b/Meshtastic/Helpers/BLEManager.swift index eded69b0..806084bb 100644 --- a/Meshtastic/Helpers/BLEManager.swift +++ b/Meshtastic/Helpers/BLEManager.swift @@ -805,7 +805,7 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate channelPacket(channel: decodedInfo.channel, fromNum: Int64(truncatingIfNeeded: connectedPeripheral.num), context: context) } // Config - if decodedInfo.config.isInitialized && !invalidVersion && connectedPeripheral != nil && self.connectedPeripheral?.num > 0 { + if decodedInfo.config.isInitialized && !invalidVersion && connectedPeripheral != nil && self.connectedPeripheral?.num != 0 { nowKnown = true localConfig(config: decodedInfo.config, context: context, nodeNum: Int64(truncatingIfNeeded: self.connectedPeripheral.num), nodeLongName: self.connectedPeripheral?.longName ?? "Unknown") } @@ -813,7 +813,7 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate if decodedInfo.moduleConfig.isInitialized && !invalidVersion && self.connectedPeripheral?.num != 0 { onWantConfigResponseReceived() nowKnown = true - moduleConfig(config: decodedInfo.moduleConfig, context: context, nodeNum: Int64(truncatingIfNeeded: self.connectedPeripheral?.num), nodeLongName: self.connectedPeripheral?.longName ?? "Unknown") + moduleConfig(config: decodedInfo.moduleConfig, context: context, nodeNum: Int64(truncatingIfNeeded: self.connectedPeripheral.num), nodeLongName: self.connectedPeripheral?.longName ?? "Unknown") if decodedInfo.moduleConfig.payloadVariant == ModuleConfig.OneOf_PayloadVariant.cannedMessage(decodedInfo.moduleConfig.cannedMessage) { if decodedInfo.moduleConfig.cannedMessage.enabled { if let validNum = self.connectedPeripheral?.num, validNum > 0 { From 5168e7f1bfe161b582651a1ba8a6ce39e76d212f Mon Sep 17 00:00:00 2001 From: Ben Meadors Date: Mon, 7 Jul 2025 12:09:41 -0500 Subject: [PATCH 199/213] Apply suggestion from @Copilot Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- Meshtastic/Helpers/BLEManager.swift | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/Meshtastic/Helpers/BLEManager.swift b/Meshtastic/Helpers/BLEManager.swift index 806084bb..4fe30485 100644 --- a/Meshtastic/Helpers/BLEManager.swift +++ b/Meshtastic/Helpers/BLEManager.swift @@ -1250,7 +1250,11 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate } else { Logger.data.warning("⚠️ connectedPeripheral is nil while attempting to log auto-favoriting a node.") } - _ = self.setFavoriteNode(node: (newMessage.toUser?.userNode)!, connectedNodeNum: fromUserNum) + guard let userNode = newMessage.toUser?.userNode else { + Logger.data.warning("⚠️ Unable to set favorite node: userNode is nil.") + return + } + _ = self.setFavoriteNode(node: userNode, connectedNodeNum: fromUserNum) } catch { context.rollback() let nsError = error as NSError From ecf54a517717baa98bf74ed5bd0e2c97cf2e348a Mon Sep 17 00:00:00 2001 From: Ben Meadors Date: Mon, 7 Jul 2025 12:10:04 -0500 Subject: [PATCH 200/213] Apply suggestion from @Copilot Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- Meshtastic/Helpers/BLEManager.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Meshtastic/Helpers/BLEManager.swift b/Meshtastic/Helpers/BLEManager.swift index 4fe30485..9fa19aab 100644 --- a/Meshtastic/Helpers/BLEManager.swift +++ b/Meshtastic/Helpers/BLEManager.swift @@ -873,11 +873,11 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate if !invalidVersion { upsertNodeInfoPacket(packet: decodedInfo.packet, context: context) } case .routingApp: if !invalidVersion { - guard let connectedPeripheral = self.connectedPeripheral else { + guard let peripheral = self.connectedPeripheral else { Logger.mesh.error("🕸️ connectedPeripheral is nil. Unable to determine connectedNodeNum for routingPacket.") return } - routingPacket(packet: decodedInfo.packet, connectedNodeNum: connectedPeripheral.num, context: context) + routingPacket(packet: decodedInfo.packet, connectedNodeNum: peripheral.num, context: context) } case .adminApp: adminAppPacket(packet: decodedInfo.packet, context: context) From c790836bba5c7a3378b027d7a3d61d71a38047ba Mon Sep 17 00:00:00 2001 From: Ben Meadors Date: Mon, 7 Jul 2025 12:14:18 -0500 Subject: [PATCH 201/213] Fix --- Meshtastic/Helpers/BLEManager.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Meshtastic/Helpers/BLEManager.swift b/Meshtastic/Helpers/BLEManager.swift index 9fa19aab..c1c26880 100644 --- a/Meshtastic/Helpers/BLEManager.swift +++ b/Meshtastic/Helpers/BLEManager.swift @@ -1252,7 +1252,7 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate } guard let userNode = newMessage.toUser?.userNode else { Logger.data.warning("⚠️ Unable to set favorite node: userNode is nil.") - return + return false } _ = self.setFavoriteNode(node: userNode, connectedNodeNum: fromUserNum) } catch { From 8fea13edf1f939c6e6583d4d4d0a3a080b54f2fb Mon Sep 17 00:00:00 2001 From: Ben Meadors Date: Mon, 7 Jul 2025 12:30:09 -0500 Subject: [PATCH 202/213] Apply suggestion from @Copilot Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- Meshtastic/Helpers/BLEManager.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Meshtastic/Helpers/BLEManager.swift b/Meshtastic/Helpers/BLEManager.swift index c1c26880..7b4c4237 100644 --- a/Meshtastic/Helpers/BLEManager.swift +++ b/Meshtastic/Helpers/BLEManager.swift @@ -816,8 +816,8 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate moduleConfig(config: decodedInfo.moduleConfig, context: context, nodeNum: Int64(truncatingIfNeeded: self.connectedPeripheral.num), nodeLongName: self.connectedPeripheral?.longName ?? "Unknown") if decodedInfo.moduleConfig.payloadVariant == ModuleConfig.OneOf_PayloadVariant.cannedMessage(decodedInfo.moduleConfig.cannedMessage) { if decodedInfo.moduleConfig.cannedMessage.enabled { - if let validNum = self.connectedPeripheral?.num, validNum > 0 { - _ = self.getCannedMessageModuleMessages(destNum: validNum, wantResponse: true) + if let connectedNum = self.connectedPeripheral?.num, connectedNum > 0 { + _ = self.getCannedMessageModuleMessages(destNum: connectedNum, wantResponse: true) } } } From e87348cc1936a74b2dc0c520e925195131d0b233 Mon Sep 17 00:00:00 2001 From: Ben Meadors Date: Mon, 7 Jul 2025 12:32:11 -0500 Subject: [PATCH 203/213] Unnecessary --- Meshtastic/Helpers/BLEManager.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Meshtastic/Helpers/BLEManager.swift b/Meshtastic/Helpers/BLEManager.swift index 7b4c4237..720ff558 100644 --- a/Meshtastic/Helpers/BLEManager.swift +++ b/Meshtastic/Helpers/BLEManager.swift @@ -1525,8 +1525,8 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate var adminPacket = AdminMessage() adminPacket.setTimeOnly = UInt32(Date().timeIntervalSince1970) var meshPacket: MeshPacket = MeshPacket() - meshPacket.to = UInt32(self.connectedPeripheral?.num ?? 0) - meshPacket.from = UInt32(self.connectedPeripheral?.num ?? 0) + meshPacket.to = UInt32(self.connectedPeripheral.num) + meshPacket.from = UInt32(self.connectedPeripheral.num) meshPacket.id = UInt32.random(in: UInt32(UInt8.max).. Date: Mon, 7 Jul 2025 14:02:19 -0700 Subject: [PATCH 204/213] Unwrap first canned messages element --- Meshtastic/Helpers/MeshPackets.swift | 4 ++-- protobufs | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Meshtastic/Helpers/MeshPackets.swift b/Meshtastic/Helpers/MeshPackets.swift index ee329f8c..58d58e14 100644 --- a/Meshtastic/Helpers/MeshPackets.swift +++ b/Meshtastic/Helpers/MeshPackets.swift @@ -517,11 +517,11 @@ func adminAppPacket (packet: MeshPacket, context: NSManagedObjectContext) { .replacingOccurrences(of: "11: ", with: "") .replacingOccurrences(of: "\"", with: "") .trimmingCharacters(in: .whitespacesAndNewlines) - .components(separatedBy: "\n")[0] + .components(separatedBy: "\n").first ?? "" fetchedNode[0].cannedMessageConfig?.messages = messages do { try context.save() - Logger.data.info("💾 Updated Canned Messages Messages For: \(fetchedNode[0].num.toHex(), privacy: .public)") + Logger.data.info("💾 Updated Canned Messages Messages For: \(fetchedNode.first?.num.toHex() ?? "Unknown".localized), privacy: .public)") } catch { context.rollback() let nsError = error as NSError diff --git a/protobufs b/protobufs index 27fac391..816595c8 160000 --- a/protobufs +++ b/protobufs @@ -1 +1 @@ -Subproject commit 27fac39141d99fe727a0a1824c5397409b1aea75 +Subproject commit 816595c8bbdfc3b4388e11348ccd043294d58705 From 1d34f3293adad8ce294735d37fc5b76561256153 Mon Sep 17 00:00:00 2001 From: Mathew Kamkar <578302+matkam@users.noreply.github.com> Date: Mon, 7 Jul 2025 21:52:45 -0700 Subject: [PATCH 205/213] simplify Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- Meshtastic/Views/Messages/UserList.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Meshtastic/Views/Messages/UserList.swift b/Meshtastic/Views/Messages/UserList.swift index 0798ffbf..4fd30445 100644 --- a/Meshtastic/Views/Messages/UserList.swift +++ b/Meshtastic/Views/Messages/UserList.swift @@ -338,7 +338,7 @@ struct FilteredUserList: View { } // Always apply unmessagable and connected node filters // Show unmessagable nodes only if they have messages, otherwise hide them - let isUnmessagablePredicate = NSPredicate(format: "unmessagable == NO OR ((SUBQUERY(receivedMessages, $msg, $msg.messageId != nil).@count > 0) OR (SUBQUERY(sentMessages, $msg, $msg.messageId != nil).@count > 0))") + let isUnmessagablePredicate = NSPredicate(format: "unmessagable == NO OR (receivedMessages.@count > 0 OR sentMessages.@count > 0)") predicates.append(isUnmessagablePredicate) let isIgnoredPredicate = NSPredicate(format: "userNode.ignored == NO") predicates.append(isIgnoredPredicate) From b2961edc533a301d1eb8f0046f883fc6ec65ed93 Mon Sep 17 00:00:00 2001 From: Mathew Kamkar <578302+matkam@users.noreply.github.com> Date: Mon, 7 Jul 2025 21:57:04 -0700 Subject: [PATCH 206/213] improved readability Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- Meshtastic/Views/Messages/UserList.swift | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/Meshtastic/Views/Messages/UserList.swift b/Meshtastic/Views/Messages/UserList.swift index 4fd30445..6d20ec23 100644 --- a/Meshtastic/Views/Messages/UserList.swift +++ b/Meshtastic/Views/Messages/UserList.swift @@ -338,7 +338,9 @@ struct FilteredUserList: View { } // Always apply unmessagable and connected node filters // Show unmessagable nodes only if they have messages, otherwise hide them - let isUnmessagablePredicate = NSPredicate(format: "unmessagable == NO OR (receivedMessages.@count > 0 OR sentMessages.@count > 0)") + let unmessagablePredicate = NSPredicate(format: "unmessagable == NO") + let hasMessagesPredicate = NSPredicate(format: "receivedMessages.@count > 0 OR sentMessages.@count > 0") + let isUnmessagablePredicate = NSCompoundPredicate(type: .or, subpredicates: [unmessagablePredicate, hasMessagesPredicate]) predicates.append(isUnmessagablePredicate) let isIgnoredPredicate = NSPredicate(format: "userNode.ignored == NO") predicates.append(isIgnoredPredicate) From fc958d6b7d4fcc1a5ea08d4526e3ebf8ef6fd135 Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Wed, 9 Jul 2025 10:08:25 -0700 Subject: [PATCH 207/213] Add a crash reporting opt out --- Localizable.xcstrings | 6 ++++++ Meshtastic/Extensions/UserDefaults.swift | 4 ++++ Meshtastic/MeshtasticApp.swift | 2 +- Meshtastic/Views/Settings/AppSettings.swift | 8 ++++++++ protobufs | 2 +- 5 files changed, 20 insertions(+), 2 deletions(-) diff --git a/Localizable.xcstrings b/Localizable.xcstrings index 219f123e..05e01396 100644 --- a/Localizable.xcstrings +++ b/Localizable.xcstrings @@ -22993,6 +22993,9 @@ } } } + }, + "Provide anonymous usage statistics and crash reports." : { + }, "Provide Confirmation" : { @@ -33157,6 +33160,9 @@ } } } + }, + "Usage and Crash Data" : { + }, "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" : { diff --git a/Meshtastic/Extensions/UserDefaults.swift b/Meshtastic/Extensions/UserDefaults.swift index 11539ab2..0ca337e9 100644 --- a/Meshtastic/Extensions/UserDefaults.swift +++ b/Meshtastic/Extensions/UserDefaults.swift @@ -74,6 +74,7 @@ extension UserDefaults { case environmentEnableWeatherKit case enableAdministration case mapReportingOptIn + case usageDataAndCrashReporting case testIntEnum } @@ -155,6 +156,9 @@ extension UserDefaults { @UserDefault(.mapReportingOptIn, defaultValue: false) static var mapReportingOptIn: Bool + + @UserDefault(.usageDataAndCrashReporting, defaultValue: true) + static var usageDataAndCrashReporting: Bool @UserDefault(.testIntEnum, defaultValue: .one) static var testIntEnum: TestIntEnum diff --git a/Meshtastic/MeshtasticApp.swift b/Meshtastic/MeshtasticApp.swift index 22ea61f3..9ce07142 100644 --- a/Meshtastic/MeshtasticApp.swift +++ b/Meshtastic/MeshtasticApp.swift @@ -41,7 +41,7 @@ struct MeshtasticAppleApp: App { env: environment, site: .us5 ), - trackingConsent: .granted + trackingConsent: UserDefaults.usageDataAndCrashReporting ? .granted : .notGranted ) RUM.enable( diff --git a/Meshtastic/Views/Settings/AppSettings.swift b/Meshtastic/Views/Settings/AppSettings.swift index 93d1e8da..eeddf8d0 100644 --- a/Meshtastic/Views/Settings/AppSettings.swift +++ b/Meshtastic/Views/Settings/AppSettings.swift @@ -16,6 +16,7 @@ struct AppSettings: View { @AppStorage("purgeStaleNodeDays") private var purgeStaleNodeDays: Double = 0 @AppStorage("environmentEnableWeatherKit") private var environmentEnableWeatherKit: Bool = true @AppStorage("enableAdministration") private var enableAdministration: Bool = false + @AppStorage("usageDataAndCrashReporting") private var usageDataAndCrashReporting: Bool = true var body: some View { VStack { Form { @@ -33,6 +34,13 @@ struct AppSettings: View { Text("PKI based node administration, requires firmware version 2.5+") .foregroundStyle(.secondary) .font(.caption) + Toggle(isOn: $usageDataAndCrashReporting) { + Label("Usage and Crash Data", systemImage: "pencil.and.list.clipboard") + } + .toggleStyle(SwitchToggleStyle(tint: .accentColor)) + Text("Provide anonymous usage statistics and crash reports.") + .foregroundStyle(.secondary) + .font(.caption) } Section(header: Text("environment")) { VStack(alignment: .leading) { diff --git a/protobufs b/protobufs index 27fac391..816595c8 160000 --- a/protobufs +++ b/protobufs @@ -1 +1 @@ -Subproject commit 27fac39141d99fe727a0a1824c5397409b1aea75 +Subproject commit 816595c8bbdfc3b4388e11348ccd043294d58705 From dd707e070eb48c07d604ecd72970489d6a9a8a43 Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Wed, 9 Jul 2025 12:08:44 -0700 Subject: [PATCH 208/213] Update device metadata messaging --- Meshtastic/Views/Settings/Config/ConfigHeader.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Meshtastic/Views/Settings/Config/ConfigHeader.swift b/Meshtastic/Views/Settings/Config/ConfigHeader.swift index d1af3a6a..cb4f7aee 100644 --- a/Meshtastic/Views/Settings/Config/ConfigHeader.swift +++ b/Meshtastic/Views/Settings/Config/ConfigHeader.swift @@ -11,7 +11,7 @@ struct ConfigHeader: View { var body: some View { if node != nil && node?.metadata == nil && node?.num ?? 0 != bleManager.connectedPeripheral?.num ?? 0 { - Text("There has been no response to a request for device metadata over the admin channel for this node.") + Text("There has been no response to a request for device metadata via PKC admin for this node.") .font(.callout) .foregroundColor(.orange) @@ -19,7 +19,7 @@ struct ConfigHeader: View { // Let users know what is going on if they are using remote admin and don't have the config yet let expiration = node?.sessionExpiration ?? Date() if node?[keyPath: config] == nil || expiration < node?.sessionExpiration ?? Date() { - Text("\(title) config data was requested over the admin channel but no response has been returned from the remote node.") + Text("\(title) config data was requested via PKC admin but no response has been returned from the remote node.") .font(.callout) .foregroundColor(.orange) } else { From 9aa41b7c7330c5f62111221a2454cd282aaed549 Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Wed, 9 Jul 2025 14:58:00 -0700 Subject: [PATCH 209/213] Update Meshtastic/Helpers/MeshPackets.swift Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- Meshtastic/Helpers/MeshPackets.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Meshtastic/Helpers/MeshPackets.swift b/Meshtastic/Helpers/MeshPackets.swift index 58d58e14..49eb61ee 100644 --- a/Meshtastic/Helpers/MeshPackets.swift +++ b/Meshtastic/Helpers/MeshPackets.swift @@ -521,7 +521,7 @@ func adminAppPacket (packet: MeshPacket, context: NSManagedObjectContext) { fetchedNode[0].cannedMessageConfig?.messages = messages do { try context.save() - Logger.data.info("💾 Updated Canned Messages Messages For: \(fetchedNode.first?.num.toHex() ?? "Unknown".localized), privacy: .public)") + Logger.data.info("💾 Updated Canned Messages Messages For: \(fetchedNode.first?.num.toHex(privacy: .public) ?? "Unknown".localized)") } catch { context.rollback() let nsError = error as NSError From 25256a27d8850f7849f4664a88edac060e8649e6 Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Wed, 9 Jul 2025 15:03:51 -0700 Subject: [PATCH 210/213] Fix copilot screwup --- Meshtastic/Helpers/MeshPackets.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Meshtastic/Helpers/MeshPackets.swift b/Meshtastic/Helpers/MeshPackets.swift index 49eb61ee..10a3d69b 100644 --- a/Meshtastic/Helpers/MeshPackets.swift +++ b/Meshtastic/Helpers/MeshPackets.swift @@ -521,7 +521,7 @@ func adminAppPacket (packet: MeshPacket, context: NSManagedObjectContext) { fetchedNode[0].cannedMessageConfig?.messages = messages do { try context.save() - Logger.data.info("💾 Updated Canned Messages Messages For: \(fetchedNode.first?.num.toHex(privacy: .public) ?? "Unknown".localized)") + Logger.data.info("💾 Updated Canned Messages Messages For: \(fetchedNode.first?.num.toHex() ?? "Unknown".localized, privacy: .public)") } catch { context.rollback() let nsError = error as NSError From 4bf5cd67f0d0ab3f2f41af032602a616b2466149 Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Wed, 9 Jul 2025 15:04:52 -0700 Subject: [PATCH 211/213] localizable file updates --- Localizable.xcstrings | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/Localizable.xcstrings b/Localizable.xcstrings index 936806ab..1bd2d536 100644 --- a/Localizable.xcstrings +++ b/Localizable.xcstrings @@ -670,6 +670,7 @@ } }, "%@ config data was requested over the admin channel but no response has been returned from the remote node." : { + "extractionState" : "stale", "localizations" : { "it" : { "stringUnit" : { @@ -702,6 +703,9 @@ } } } + }, + "%@ config data was requested via PKC admin but no response has been returned from the remote node." : { + }, "%@ dB" : { "localizations" : { @@ -2043,6 +2047,7 @@ } }, "A channel index of 0 indicates the primary channel where all broadcast packets are sent from." : { + "extractionState" : "stale", "localizations" : { "ja" : { "stringUnit" : { @@ -2051,6 +2056,9 @@ } } } + }, + "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." : { + }, "A green lock means the channel is securely encrypted with either a 128 or 256 bit AES key." : { "localizations" : { @@ -2127,6 +2135,7 @@ } }, "A red lock with a slash means the channel is not securely encrypted, it uses either no key at all or a 1 byte known key. Traffic on this channel is easily intercepted." : { + "extractionState" : "stale", "localizations" : { "ja" : { "stringUnit" : { @@ -2135,6 +2144,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." : { + + }, + "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." : { + }, "A Trace Route was sent, no response has been received." : { "localizations" : { @@ -36384,6 +36399,7 @@ } }, "There has been no response to a request for device metadata over the admin channel for this node." : { + "extractionState" : "stale", "localizations" : { "it" : { "stringUnit" : { @@ -36410,6 +36426,9 @@ } } } + }, + "There has been no response to a request for device metadata via PKC admin for this node." : { + }, "These settings will %@ channels. The current LoRa Config will be replaced, if there are substantial changes to the LoRa config the device will reboot" : { "localizations" : { From 70ef1108cc44be559cd0864ccf905e8256ea79bb Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Wed, 9 Jul 2025 15:07:23 -0700 Subject: [PATCH 212/213] Delete stale keys --- Localizable.xcstrings | 167 ------------------------------------------ 1 file changed, 167 deletions(-) diff --git a/Localizable.xcstrings b/Localizable.xcstrings index 1bd2d536..c04d1475 100644 --- a/Localizable.xcstrings +++ b/Localizable.xcstrings @@ -669,41 +669,6 @@ } } }, - "%@ config data was requested over the admin channel but no response has been returned from the remote node." : { - "extractionState" : "stale", - "localizations" : { - "it" : { - "stringUnit" : { - "state" : "translated", - "value" : "i dati di configurazione %@ sono stati richiesti attraverso il canale di amministrazione, ma non è stata fornita alcuna risposta dal nodo remoto." - } - }, - "ja" : { - "stringUnit" : { - "state" : "translated", - "value" : "%@ 設定データが管理チャンネル経由で要求されましたが、リモートノードからの応答がありません。" - } - }, - "sr" : { - "stringUnit" : { - "state" : "translated", - "value" : "%@ конфигурациони подаци су затражени преко административног канала, али никакав одговор није враћен са удаљеног чвора." - } - }, - "zh-Hans" : { - "stringUnit" : { - "state" : "translated", - "value" : "已通过管理频道请求 %@ 配置数据,但远程节点未返回任何响应。" - } - }, - "zh-Hant-TW" : { - "stringUnit" : { - "state" : "translated", - "value" : "透過管理通道請求 %@ 組態資料,但遠端節點未回應。" - } - } - } - }, "%@ config data was requested via PKC admin but no response has been returned from the remote node." : { }, @@ -2046,17 +2011,6 @@ } } }, - "A channel index of 0 indicates the primary channel where all broadcast packets are sent from." : { - "extractionState" : "stale", - "localizations" : { - "ja" : { - "stringUnit" : { - "state" : "translated", - "value" : "チャンネルインデックス0は、すべてのブロードキャストパケットが送信されるプライマリチャンネルを示します。" - } - } - } - }, "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." : { }, @@ -2134,17 +2088,6 @@ } } }, - "A red lock with a slash means the channel is not securely encrypted, it uses either no key at all or a 1 byte known key. Traffic on this channel is easily intercepted." : { - "extractionState" : "stale", - "localizations" : { - "ja" : { - "stringUnit" : { - "state" : "translated", - "value" : "斜線付きの赤い鍵は、チャンネルが安全に暗号化されていないことを意味し、キーが全くないか1バイトの既知キーを使用しています。このチャンネルのトラフィックは簡単に傍受されます。" - } - } - } - }, "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." : { }, @@ -31348,35 +31291,6 @@ } } }, - "Send a Direct Message" : { - "extractionState" : "stale", - "localizations" : { - "it" : { - "stringUnit" : { - "state" : "translated", - "value" : "Inviare un messaggio diretto" - } - }, - "ja" : { - "stringUnit" : { - "state" : "translated", - "value" : "ダイレクトメッセージを送信" - } - }, - "sr" : { - "stringUnit" : { - "state" : "translated", - "value" : "Пошаљи директну поруку" - } - }, - "zh-Hant-TW" : { - "stringUnit" : { - "state" : "translated", - "value" : "發送私訊" - } - } - } - }, "Send a Group Message" : { "localizations" : { "de" : { @@ -31461,35 +31375,6 @@ } } }, - "Send a message to a certain meshtastic node" : { - "extractionState" : "stale", - "localizations" : { - "it" : { - "stringUnit" : { - "state" : "translated", - "value" : "Inviare un messaggio a un certo nodo meshtastico" - } - }, - "ja" : { - "stringUnit" : { - "state" : "translated", - "value" : "特定のMeshtasticノードにメッセージを送信" - } - }, - "sr" : { - "stringUnit" : { - "state" : "translated", - "value" : "Пошаљи поруку одређеном мештастик чвору" - } - }, - "zh-Hant-TW" : { - "stringUnit" : { - "state" : "translated", - "value" : "向特定 Meshtastic 節點發送訊息" - } - } - } - }, "Send a position on the primary channel when the user button is triple clicked." : { "localizations" : { "it" : { @@ -33552,29 +33437,6 @@ } } }, - "Shows information for the Lora radio connected via bluetooth. You can swipe left to disconnect the radio and long press start the live activity." : { - "extractionState" : "stale", - "localizations" : { - "it" : { - "stringUnit" : { - "state" : "translated", - "value" : "Mostra le informazioni relative alla radio Lora collegata via bluetooth. È possibile scorrere il dito verso sinistra per scollegare la radio e premere a lungo per avviare l'attività live." - } - }, - "ja" : { - "stringUnit" : { - "state" : "translated", - "value" : "Bluetoothで接続されたLoRaラジオの情報を表示します。左にスワイプしてラジオを切断し、長押しでライブアクティビティを開始できます。" - } - }, - "zh-Hant-TW" : { - "stringUnit" : { - "state" : "translated", - "value" : "顯示透過藍牙連接的 LoRa 裝置資訊。您可以向左滑動來斷開裝置,長按則可啟動即時活動。" - } - } - } - }, "Shows information for the Lora radio connected via bluetooth. You can swipe left to disconnect the radio and long press to start the live activity." : { }, @@ -36398,35 +36260,6 @@ } } }, - "There has been no response to a request for device metadata over the admin channel for this node." : { - "extractionState" : "stale", - "localizations" : { - "it" : { - "stringUnit" : { - "state" : "translated", - "value" : "Non è stata data risposta a una richiesta di metadati del dispositivo sul canale di amministrazione per questo nodo." - } - }, - "ja" : { - "stringUnit" : { - "state" : "translated", - "value" : "このノードの管理チャンネル経由でのデバイスメタデータ要求に対する応答がありません。" - } - }, - "sr" : { - "stringUnit" : { - "state" : "translated", - "value" : "Није било одговора на захтев за метаподатке уређаја преко административног канала за овај чвор." - } - }, - "zh-Hant-TW" : { - "stringUnit" : { - "state" : "translated", - "value" : "此節點的管理頻道未回應設備中繼資料的請求。" - } - } - } - }, "There has been no response to a request for device metadata via PKC admin for this node." : { }, From 6a4e353bde8b867e64ffd162a5f0baede7199b3c Mon Sep 17 00:00:00 2001 From: Ben Meadors Date: Thu, 10 Jul 2025 11:35:11 -0500 Subject: [PATCH 213/213] Add dSYM file generation for DataDog --- .github/workflows/macos-dSYM.yml | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) create mode 100644 .github/workflows/macos-dSYM.yml diff --git a/.github/workflows/macos-dSYM.yml b/.github/workflows/macos-dSYM.yml new file mode 100644 index 00000000..cb490792 --- /dev/null +++ b/.github/workflows/macos-dSYM.yml @@ -0,0 +1,21 @@ +name: Upload dSYM Files + +jobs: + build: + runs-on: macos-latest + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Generate/Download dSYM Files + uses: ./release.sh + + - name: Upload dSYMs to Datadog + uses: DataDog/upload-dsyms-github-action@v1 + with: + api_key: ${{ secrets.DATADOG_API_KEY }} + site: datadoghq.com + dsym_paths: | + path/to/dsyms/folder + path/to/zip/dsyms.zip \ No newline at end of file