From 5fb351b2f17dc91bf22118df3016306c6f0c77f2 Mon Sep 17 00:00:00 2001 From: Jake-B Date: Sun, 26 Oct 2025 15:03:19 -0400 Subject: [PATCH] More descriptive manual connection rows --- Localizable.xcstrings | 7 +++ Meshtastic.xcodeproj/project.pbxproj | 4 ++ .../AccessoryManager+Connect.swift | 17 ++---- .../AccessoryManager+FromRadio.swift | 9 +++ .../Accessory Manager/AccessoryManager.swift | 6 +- .../Helpers/ManualConnectionList.swift | 61 +++++++++++++++++++ Meshtastic/Accessory/Protocols/Device.swift | 15 ++++- Meshtastic/Views/Connect/Connect.swift | 46 +++++++++++--- 8 files changed, 142 insertions(+), 23 deletions(-) create mode 100644 Meshtastic/Accessory/Helpers/ManualConnectionList.swift diff --git a/Localizable.xcstrings b/Localizable.xcstrings index 5ec975ae..6f4a50b6 100644 --- a/Localizable.xcstrings +++ b/Localizable.xcstrings @@ -18921,6 +18921,13 @@ } } } + }, + "Last seen device:" : { + "comment" : "A label displayed next to the last seen device text in the `DeviceConnectRow`.", + "isCommentAutoGenerated" : true + }, + "Last seen device: %@" : { + }, "Latitude" : { "localizations" : { diff --git a/Meshtastic.xcodeproj/project.pbxproj b/Meshtastic.xcodeproj/project.pbxproj index 63f334e5..2fd4cb6e 100644 --- a/Meshtastic.xcodeproj/project.pbxproj +++ b/Meshtastic.xcodeproj/project.pbxproj @@ -42,6 +42,7 @@ 2344A2B12D68DFF800170A77 /* Constants.swift in Sources */ = {isa = PBXBuildFile; fileRef = 25C49D8F2C471AEA0024FBD1 /* Constants.swift */; }; 2346A7192E2FB9A300CB9239 /* SerialConnection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2346A7182E2FB9A300CB9239 /* SerialConnection.swift */; }; 2346A71D2E2FB9C500CB9239 /* SerialTransport.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2346A71C2E2FB9C500CB9239 /* SerialTransport.swift */; }; + 2349A04A2EAE4DA30060A581 /* ManualConnectionList.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2349A0492EAE4DA30060A581 /* ManualConnectionList.swift */; }; 2373AE132D0A216C0086C749 /* MetricsChartSeries.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2373AE122D0A216C0086C749 /* MetricsChartSeries.swift */; }; 2373AE152D0A24930086C749 /* MetricsSeriesList.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2373AE142D0A24930086C749 /* MetricsSeriesList.swift */; }; 2373AE172D0A26620086C749 /* EnvironmentDefaultSeries.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2373AE162D0A26620086C749 /* EnvironmentDefaultSeries.swift */; }; @@ -357,6 +358,7 @@ 2344A2AE2D6697A700170A77 /* TelemetryEntity+CoreDataProperties.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "TelemetryEntity+CoreDataProperties.swift"; sourceTree = ""; }; 2346A7182E2FB9A300CB9239 /* SerialConnection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SerialConnection.swift; sourceTree = ""; }; 2346A71C2E2FB9C500CB9239 /* SerialTransport.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SerialTransport.swift; sourceTree = ""; }; + 2349A0492EAE4DA30060A581 /* ManualConnectionList.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ManualConnectionList.swift; sourceTree = ""; }; 2373AE122D0A216C0086C749 /* MetricsChartSeries.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MetricsChartSeries.swift; sourceTree = ""; }; 2373AE142D0A24930086C749 /* MetricsSeriesList.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MetricsSeriesList.swift; sourceTree = ""; }; 2373AE162D0A26620086C749 /* EnvironmentDefaultSeries.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EnvironmentDefaultSeries.swift; sourceTree = ""; }; @@ -832,6 +834,7 @@ 23D316922E5618D2002FA4FB /* AsyncGate.swift */, 23E23F912E392C2B00919073 /* LogRecord+StringRepresentation.swift */, 23D9D9382E50DA97005D1C18 /* ResettableTimer.swift */, + 2349A0492EAE4DA30060A581 /* ManualConnectionList.swift */, ); path = Helpers; sourceTree = ""; @@ -1841,6 +1844,7 @@ BCDDFA9A2DBB180D0065189C /* ScrollToBottomButton.swift in Sources */, DDC4C9FF2A8D982900CE201C /* DetectionSensorConfig.swift in Sources */, 2344A2AF2D6697A700170A77 /* TelemetryEntity+CoreDataClass.swift in Sources */, + 2349A04A2EAE4DA30060A581 /* ManualConnectionList.swift in Sources */, 2344A2B02D6697A700170A77 /* TelemetryEntity+CoreDataProperties.swift in Sources */, D9C983A22B79D1A600BDBE6A /* RequestPositionButton.swift in Sources */, 237AEB8F2E1FE457003B7CE3 /* Transport.swift in Sources */, diff --git a/Meshtastic/Accessory/Accessory Manager/AccessoryManager+Connect.swift b/Meshtastic/Accessory/Accessory Manager/AccessoryManager+Connect.swift index 259b9c7b..d63f8acf 100644 --- a/Meshtastic/Accessory/Accessory Manager/AccessoryManager+Connect.swift +++ b/Meshtastic/Accessory/Accessory Manager/AccessoryManager+Connect.swift @@ -175,8 +175,12 @@ extension AccessoryManager { self.updateState(.subscribed) // If we successfully connected to a manual connection, then save it to the list - if device.isManualConnection { - self.saveManualConnection(device: device) + // Remember, Device is a value type (struct) so don't use use `device` here, thats + // The value at the instantiation of the connect process. We want the currently + // updated device object in `activeConnection` with its additonal metadata from + // NodeInfo packets. + if let activeDevice = self.activeConnection?.device, activeDevice.isManualConnection { + ManualConnectionList.shared.insert(device: activeDevice) } } @@ -224,15 +228,6 @@ extension AccessoryManager { // All done, one way or another, clean up self.connectionStepper = nil } - - fileprivate func saveManualConnection(device: Device) { - var manualConnections = UserDefaults.manualConnections - - if manualConnections.first(where: {$0.id == device.id}) == nil { - manualConnections.append(device) - UserDefaults.manualConnections = manualConnections - } - } } // Sequentially stepped tasks diff --git a/Meshtastic/Accessory/Accessory Manager/AccessoryManager+FromRadio.swift b/Meshtastic/Accessory/Accessory Manager/AccessoryManager+FromRadio.swift index 01cdac79..5395f676 100644 --- a/Meshtastic/Accessory/Accessory Manager/AccessoryManager+FromRadio.swift +++ b/Meshtastic/Accessory/Accessory Manager/AccessoryManager+FromRadio.swift @@ -113,6 +113,15 @@ extension AccessoryManager { updateDevice(deviceId: activeDevice.id, key: \.shortName, value: user.shortName ?? "?") updateDevice(deviceId: activeDevice.id, key: \.longName, value: user.longName ?? "Unknown".localized) updateDevice(deviceId: activeDevice.id, key: \.hardwareModel, value: user.hwModel) + + if activeDevice.isManualConnection { + // We just received a NodeInfo for the currently connected node and this is a + // manual connection. Update the metadata for the device entry in UserDefaults + // with this information for better display later + ManualConnectionList.shared.updateDevice(deviceId: activeDevice.id, key: \.shortName, value: user.shortName) + ManualConnectionList.shared.updateDevice(deviceId: activeDevice.id, key: \.longName, value: user.longName) + ManualConnectionList.shared.updateDevice(deviceId: activeDevice.id, key: \.hardwareModel, value: user.hwModel) + } } } } diff --git a/Meshtastic/Accessory/Accessory Manager/AccessoryManager.swift b/Meshtastic/Accessory/Accessory Manager/AccessoryManager.swift index a723b54d..a6857f02 100644 --- a/Meshtastic/Accessory/Accessory Manager/AccessoryManager.swift +++ b/Meshtastic/Accessory/Accessory Manager/AccessoryManager.swift @@ -302,9 +302,9 @@ class AccessoryManager: ObservableObject, MqttClientProxyManagerDelegate { Logger.transport.error("updateDevice with nil deviceId") return } - - // Update the active device - if let activeConnection { + + // Update the active device if the UUID's match + if let activeConnection, activeConnection.device.id == deviceId { var device = activeConnection.device if device[keyPath: key] != value { // Update the @Published stuff for the UI diff --git a/Meshtastic/Accessory/Helpers/ManualConnectionList.swift b/Meshtastic/Accessory/Helpers/ManualConnectionList.swift new file mode 100644 index 00000000..665573a9 --- /dev/null +++ b/Meshtastic/Accessory/Helpers/ManualConnectionList.swift @@ -0,0 +1,61 @@ +// +// ManualConnectionList.swift +// Meshtastic +// +// Created by jake on 10/26/25. +// + +import Foundation + +// Maintains an observable list of devices that's backed by UserDefaults +public class ManualConnectionList: ObservableObject { + static let shared = ManualConnectionList() + + @Published private var _list: [Device] + + private init() { + _list = UserDefaults.manualConnections + } + + var connectionsList: [Device] { + get { + return _list + } + } + + func insert(device: Device) { + // Don't insert if already there + guard !_list.contains(where: {$0.id == device.id}) else { + return + } + + // Add the new entry + var list = _list + list.append(device) + _list = list + UserDefaults.manualConnections = list + } + + func updateDevice(deviceId: UUID, key: WritableKeyPath, value: T) where T: Equatable { + var list = _list + if let deviceIndex = list.firstIndex(where: {$0.id == deviceId}) { + list[deviceIndex][keyPath: key] = value + _list = list + UserDefaults.manualConnections = list + } + } + + func remove(device: Device) { + var list = _list + list.removeAll(where: {$0.id == device.id}) + _list = list + UserDefaults.manualConnections = list + } + + func remove(atOffsets: IndexSet) { + var list = _list + list.remove(atOffsets: atOffsets) + _list = list + UserDefaults.manualConnections = list + } +} diff --git a/Meshtastic/Accessory/Protocols/Device.swift b/Meshtastic/Accessory/Protocols/Device.swift index a5c900f0..d14b349e 100644 --- a/Meshtastic/Accessory/Protocols/Device.swift +++ b/Meshtastic/Accessory/Protocols/Device.swift @@ -7,7 +7,8 @@ import Foundation -struct Device: Identifiable, Hashable, Codable { +struct Device: Identifiable, Hashable, Codable, CustomStringConvertible { + let id: UUID var name: String var transportType: TransportType @@ -56,4 +57,16 @@ struct Device: Identifiable, Hashable, Codable { } } + var description: String { + switch (shortName, longName) { + case (let shortName?, let longName?): // Both shortName and longName are non-nil + return "\(longName) (\(shortName))" + case (let shortName?, nil): // shortName is non-nil, longName is nil + return "\(shortName)" + case (nil, let longName?): // shortName is nil, longName is non-nil + return "\(longName)" + default: // Both are nil + return "Device(id: \(id))" + } + } } diff --git a/Meshtastic/Views/Connect/Connect.swift b/Meshtastic/Views/Connect/Connect.swift index 6ac3106a..bb43ae04 100644 --- a/Meshtastic/Views/Connect/Connect.swift +++ b/Meshtastic/Views/Connect/Connect.swift @@ -26,6 +26,7 @@ struct Connect: View { @State var isUnsetRegion = false @State var invalidFirmwareVersion = false @State var liveActivityStarted = false + @ObservedObject var manualConnections = ManualConnectionList.shared var body: some View { NavigationStack { @@ -272,15 +273,23 @@ struct Connect: View { DeviceConnectRow(device: device) } } - if UserDefaults.manualConnections.count > 0 { + if manualConnections.connectionsList.count > 0 { Section(header: Text("Manual Connections").font(.title)) { - ForEach(UserDefaults.manualConnections) { device in + ForEach(manualConnections.connectionsList) { device in DeviceConnectRow(device: device) +#if targetEnvironment(macCatalyst) + .contextMenu { + Button { + manualConnections.remove(device: device) + } label: { + Label("Delete", systemImage: "trash") + } + } +#endif }.onDelete { offsets in - var list = UserDefaults.manualConnections - list.remove(atOffsets: offsets) - UserDefaults.manualConnections = list + manualConnections.remove(atOffsets: offsets) } + } } } @@ -526,7 +535,7 @@ struct DeviceConnectRow: View { @Environment(\.managedObjectContext) var context @EnvironmentObject var accessoryManager: AccessoryManager @State var presentingSwitchPreferredPeripheral = false - var device: Device + let device: Device var body: some View { HStack { @@ -555,11 +564,31 @@ struct DeviceConnectRow: View { Text(device.name).font(.callout) } // Show transport type - TransportIcon(transportType: device.transportType) +#if !targetEnvironment(macCatalyst) + HStack(alignment: .center){ + TransportIcon(transportType: device.transportType) + if device.isManualConnection && (device.longName != nil || device.shortName != nil) { + VStack (alignment: .leading) { + Text("Last seen device:") + Text("\(String(describing: device))") + } + } + }.padding(.top, 3.0) +#else + //Different alignment for Mac + HStack(alignment: .firstTextBaseline){ + TransportIcon(transportType: device.transportType) + if device.isManualConnection && (device.longName != nil || device.shortName != nil) { + Text("Last seen device: \(String(describing: device))") + } + } +#endif } Spacer() VStack { - device.getSignalStrength().map { SignalStrengthIndicator(signalStrength: $0) } + device.getSignalStrength().map { + SignalStrengthIndicator(signalStrength: $0) + } } }.padding([.bottom, .top]) .confirmationDialog("Connecting to a new radio will clear all app data on the phone.", isPresented: $presentingSwitchPreferredPeripheral, titleVisibility: .visible) { @@ -578,3 +607,4 @@ struct DeviceConnectRow: View { } } } +