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 01/68] 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 02/68] 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 03/68] 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 04/68] 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 05/68] 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 06/68] 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 07/68] 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 08/68] 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 dcfad05599770b02e7516abed39fd2c04d40e08b Mon Sep 17 00:00:00 2001 From: Jake-B Date: Fri, 9 May 2025 16:46:08 -0400 Subject: [PATCH 09/68] 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 10/68] 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 11/68] 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 12/68] Bump Version --- Meshtastic.xcodeproj/project.pbxproj | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Meshtastic.xcodeproj/project.pbxproj b/Meshtastic.xcodeproj/project.pbxproj index 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 13/68] 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 14/68] 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 15/68] 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 16/68] 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 17/68] 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 18/68] 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 19/68] 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 20/68] 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 21/68] 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 22/68] 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 23/68] 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 24/68] 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 25/68] 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 26/68] 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 27/68] 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 28/68] 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 29/68] 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 30/68] 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 31/68] 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 32/68] 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 33/68] 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 34/68] 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 35/68] 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 36/68] 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 37/68] 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 38/68] 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 39/68] 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 40/68] 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 41/68] 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 42/68] 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 43/68] 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 44/68] 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 45/68] 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 46/68] 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 47/68] 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 48/68] 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 49/68] 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 50/68] 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 51/68] 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 52/68] 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 53/68] 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 54/68] 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 55/68] 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 56/68] 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 57/68] 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 58/68] 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 59/68] 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 60/68] 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 61/68] 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 62/68] 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 63/68] 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 64/68] 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 65/68] 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 66/68] 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 67/68] 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 68/68] 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 {