More descriptive manual connection rows

This commit is contained in:
Jake-B 2025-10-26 15:03:19 -04:00
parent 486b4c1821
commit 5fb351b2f1
8 changed files with 142 additions and 23 deletions

View file

@ -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" : {

View file

@ -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 = "<group>"; };
2346A7182E2FB9A300CB9239 /* SerialConnection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SerialConnection.swift; sourceTree = "<group>"; };
2346A71C2E2FB9C500CB9239 /* SerialTransport.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SerialTransport.swift; sourceTree = "<group>"; };
2349A0492EAE4DA30060A581 /* ManualConnectionList.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ManualConnectionList.swift; sourceTree = "<group>"; };
2373AE122D0A216C0086C749 /* MetricsChartSeries.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MetricsChartSeries.swift; sourceTree = "<group>"; };
2373AE142D0A24930086C749 /* MetricsSeriesList.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MetricsSeriesList.swift; sourceTree = "<group>"; };
2373AE162D0A26620086C749 /* EnvironmentDefaultSeries.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EnvironmentDefaultSeries.swift; sourceTree = "<group>"; };
@ -832,6 +834,7 @@
23D316922E5618D2002FA4FB /* AsyncGate.swift */,
23E23F912E392C2B00919073 /* LogRecord+StringRepresentation.swift */,
23D9D9382E50DA97005D1C18 /* ResettableTimer.swift */,
2349A0492EAE4DA30060A581 /* ManualConnectionList.swift */,
);
path = Helpers;
sourceTree = "<group>";
@ -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 */,

View file

@ -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

View file

@ -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)
}
}
}
}

View file

@ -302,9 +302,9 @@ class AccessoryManager: ObservableObject, MqttClientProxyManagerDelegate {
Logger.transport.error("updateDevice<T> 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

View file

@ -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<T>(deviceId: UUID, key: WritableKeyPath<Device, T>, 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
}
}

View file

@ -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))"
}
}
}

View file

@ -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 {
}
}
}