mirror of
https://github.com/meshtastic/Meshtastic-Apple.git
synced 2026-04-20 22:13:56 +00:00
Keep list of previous manual connections
This commit is contained in:
parent
8f9be79c55
commit
486b4c1821
12 changed files with 252 additions and 90 deletions
|
|
@ -3905,10 +3905,6 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"Anonymous Usage and Crash data" : {
|
||||
"comment" : "A description of how the app collects and uses data about its usage and crashes. It emphasizes that this data is anonymous and non-personally identifiable.",
|
||||
"isCommentAutoGenerated" : true
|
||||
},
|
||||
"Any missed messages will be delivered again." : {
|
||||
"localizations" : {
|
||||
"it" : {
|
||||
|
|
@ -20445,6 +20441,9 @@
|
|||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"Manual Connections" : {
|
||||
|
||||
},
|
||||
"Map Data" : {
|
||||
"localizations" : {
|
||||
|
|
@ -20930,6 +20929,10 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"Meshtastic does not collect any personal information. We do anonymously collect usage and crash data to improve the app. This helps us understand how the app is being used and where we can make improvements. The data we collect is non-personally identifiable and cannot be linked to you as an individual. You can opt out of this under app settings." : {
|
||||
"comment" : "Privacy policy text for Meshtastic.",
|
||||
"isCommentAutoGenerated" : true
|
||||
},
|
||||
"Meshtastic Node %@ has shared channels with you" : {
|
||||
"localizations" : {
|
||||
"de" : {
|
||||
|
|
@ -40176,6 +40179,9 @@
|
|||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"User Privacy" : {
|
||||
|
||||
},
|
||||
"User Uploaded" : {
|
||||
"comment" : "Data source label for user uploaded files",
|
||||
|
|
@ -41040,10 +41046,6 @@
|
|||
},
|
||||
"Waypoints" : {
|
||||
|
||||
},
|
||||
"We anonymously collect usage and crash data to improve the app. This helps us understand how the app is being used and where we can make improvements. The data we collect is non-personally identifiable and cannot be linked to you as an individual. You can opt out of this under app settings." : {
|
||||
"comment" : "A description of how the app collects and uses user data. Includes a link to the app settings.",
|
||||
"isCommentAutoGenerated" : true
|
||||
},
|
||||
"Weather Conditions" : {
|
||||
"localizations" : {
|
||||
|
|
|
|||
|
|
@ -67,7 +67,12 @@ extension AccessoryManager {
|
|||
}
|
||||
self.activeConnection = (device: device, connection: connection)
|
||||
|
||||
if UserDefaults.preferredPeripheralId.count < 1 {
|
||||
// if we don't have a peripheralId, set it now at the beginning of the
|
||||
// connect process (because I think it is used in other parts of the app
|
||||
// during the connect process?
|
||||
// Otherwise, UserDefault.preferredPeripheralId is set in the Connect
|
||||
// view, as part of the "Connect to new radio?" confirmation dialog logic.
|
||||
if UserDefaults.preferredPeripheralId.isEmpty {
|
||||
UserDefaults.preferredPeripheralId = device.id.uuidString
|
||||
}
|
||||
} catch let error as CBError where error.code == .peerRemovedPairingInformation {
|
||||
|
|
@ -168,6 +173,11 @@ extension AccessoryManager {
|
|||
// We have an active connection
|
||||
self.updateDevice(deviceId: device.id, key: \.connectionState, value: .connected)
|
||||
self.updateState(.subscribed)
|
||||
|
||||
// If we successfully connected to a manual connection, then save it to the list
|
||||
if device.isManualConnection {
|
||||
self.saveManualConnection(device: device)
|
||||
}
|
||||
}
|
||||
|
||||
// Step 8: Update UI and status to connected
|
||||
|
|
@ -214,6 +224,15 @@ 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
|
||||
|
|
|
|||
|
|
@ -703,7 +703,8 @@ extension AccessoryManager {
|
|||
await self.heartbeatTimer?.cancel(withReason: "Duplicate setup, cancelling previous timer")
|
||||
self.heartbeatTimer = nil
|
||||
}
|
||||
self.heartbeatTimer = ResettableTimer(isRepeating: true, debugName: "Send Heartbeat") {
|
||||
|
||||
self.heartbeatTimer = ResettableTimer(isRepeating: true, debugName: Bundle.main.isDebug ? "Send Heartbeat" : nil) {
|
||||
Logger.transport.debug("💓 [Heartbeat] Sending periodic heartbeat")
|
||||
try? await self.sendHeartbeat()
|
||||
}
|
||||
|
|
@ -711,7 +712,7 @@ extension AccessoryManager {
|
|||
// We can send heartbeats for older versions just fine, but only 2.7.4 and up will respond with
|
||||
// a definite queueStatus packet.
|
||||
if self.checkIsVersionSupported(forVersion: "2.7.4") {
|
||||
self.heartbeatResponseTimer = ResettableTimer(isRepeating: false, debugName: "Heartbeat Timeout") { @MainActor in
|
||||
self.heartbeatResponseTimer = ResettableTimer(isRepeating: false, debugName: Bundle.main.isDebug ? "Heartbeat Timeout" : nil) { @MainActor in
|
||||
Logger.transport.error("💓 [Heartbeat] Connection Timeout: Did not receive a packet after heartbeat.")
|
||||
// If we're in the middle of a connection cancel it.
|
||||
await self.connectionStepper?.cancel()
|
||||
|
|
|
|||
|
|
@ -31,7 +31,7 @@ enum ConnectionEvent {
|
|||
case disconnected(shouldReconnect: Bool)
|
||||
}
|
||||
|
||||
enum ConnectionState: Equatable {
|
||||
enum ConnectionState: Equatable, Codable {
|
||||
case disconnected
|
||||
case connecting
|
||||
case connected
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@
|
|||
|
||||
import Foundation
|
||||
|
||||
struct Device: Identifiable, Hashable {
|
||||
struct Device: Identifiable, Hashable, Codable {
|
||||
let id: UUID
|
||||
var name: String
|
||||
var transportType: TransportType
|
||||
|
|
@ -23,7 +23,9 @@ struct Device: Identifiable, Hashable {
|
|||
|
||||
var connectionState: ConnectionState
|
||||
var wasRestored: Bool = false
|
||||
init(id: UUID, name: String, transportType: TransportType, identifier: String, connectionState: ConnectionState = .disconnected, rssi: Int? = nil, num: Int64? = nil, wasRestored: Bool = false) {
|
||||
var isManualConnection: Bool = false
|
||||
|
||||
init(id: UUID, name: String, transportType: TransportType, identifier: String, connectionState: ConnectionState = .disconnected, rssi: Int? = nil, num: Int64? = nil, wasRestored: Bool = false, isManualConnection: Bool = false) {
|
||||
self.id = id
|
||||
self.name = name
|
||||
self.transportType = transportType
|
||||
|
|
@ -32,6 +34,7 @@ struct Device: Identifiable, Hashable {
|
|||
self.rssi = rssi
|
||||
self.num = num
|
||||
self.wasRestored = wasRestored
|
||||
self.isManualConnection = isManualConnection
|
||||
}
|
||||
|
||||
var rssiString: String {
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ import Foundation
|
|||
import CommonCrypto
|
||||
import SwiftUI
|
||||
|
||||
enum TransportType: String, CaseIterable {
|
||||
enum TransportType: String, CaseIterable, Codable {
|
||||
case ble = "BLE"
|
||||
case tcp = "TCP"
|
||||
case serial = "Serial"
|
||||
|
|
@ -53,14 +53,15 @@ protocol Transport {
|
|||
var requiresPeriodicHeartbeat: Bool { get }
|
||||
var supportsManualConnection: Bool { get }
|
||||
|
||||
func manuallyConnect(withConnectionString: String) async throws
|
||||
func device(forManualConnection: String) -> Device?
|
||||
func manuallyConnect(toDevice: Device) async throws
|
||||
}
|
||||
|
||||
// Used to make stable-ish ID's for accessories that don't have a UUID
|
||||
extension String {
|
||||
func toUUIDFormatHash() -> UUID? {
|
||||
func toUUIDFormatHash() -> UUID {
|
||||
// Convert string to data
|
||||
guard let data = self.data(using: .utf8) else { return nil }
|
||||
let data = self.data(using: .utf8) ?? Data()
|
||||
|
||||
// Create buffer for SHA-256 hash (32 bytes)
|
||||
var digest = [UInt8](repeating: 0, count: Int(CC_SHA256_DIGEST_LENGTH))
|
||||
|
|
|
|||
|
|
@ -403,7 +403,11 @@ class BLETransport: Transport {
|
|||
|
||||
}
|
||||
|
||||
func manuallyConnect(withConnectionString: String) async throws {
|
||||
func device(forManualConnection: String) -> Device? {
|
||||
return nil
|
||||
}
|
||||
|
||||
func manuallyConnect(toDevice: Device) async throws {
|
||||
Logger.transport.error("🛜 [BLE] This transport does not support manual connections")
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -31,7 +31,7 @@ class SerialTransport: Transport {
|
|||
while !Task.isCancelled {
|
||||
let ports = self.getSerialPorts()
|
||||
for port in ports {
|
||||
let id = port.toUUIDFormatHash() ?? UUID()
|
||||
let id = port.toUUIDFormatHash()
|
||||
if !portsAlreadyNotified.contains(port) {
|
||||
Logger.transport.info("🔱 [Serial] Port \(port, privacy: .public) found.")
|
||||
let newDevice = Device(id: id,
|
||||
|
|
@ -45,9 +45,8 @@ class SerialTransport: Transport {
|
|||
for knownPort in portsAlreadyNotified where !ports.contains(knownPort) {
|
||||
// Previosuly seen port is no longer available
|
||||
Logger.transport.info("🔱 [Serial] Port \(knownPort, privacy: .public) is no longer connected.")
|
||||
if let uuid = knownPort.toUUIDFormatHash() {
|
||||
cont.yield(.deviceLost(uuid))
|
||||
}
|
||||
let uuid = knownPort.toUUIDFormatHash()
|
||||
cont.yield(.deviceLost(uuid))
|
||||
portsAlreadyNotified.removeAll(where: {$0 == knownPort})
|
||||
}
|
||||
try? await Task.sleep(for: .seconds(5))
|
||||
|
|
@ -118,7 +117,11 @@ class SerialTransport: Transport {
|
|||
return SerialConnection(path: device.identifier)
|
||||
}
|
||||
|
||||
func manuallyConnect(withConnectionString: String) async throws {
|
||||
func device(forManualConnection: String) -> Device? {
|
||||
return nil
|
||||
}
|
||||
|
||||
func manuallyConnect(toDevice: Device) async throws {
|
||||
Logger.transport.error("🔱 [USB] This transport does not support manual connections")
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -73,7 +73,7 @@ class TCPTransport: NSObject, Transport, NetServiceBrowserDelegate, NetServiceDe
|
|||
let ip = service.ipv4Address ?? "Unknown IP"
|
||||
|
||||
// Use a mishmash of things and hash for stable? ID.
|
||||
let idString = "\(service.name):\(host):\(ip):\(port)".toUUIDFormatHash() ?? UUID()
|
||||
let idString = "\(service.name):\(host):\(ip):\(port)".toUUIDFormatHash()
|
||||
|
||||
// Save the resolved service locally for later
|
||||
services[service.name] = ResolvedService(id: idString, service: service, host: host, port: port)
|
||||
|
|
@ -134,16 +134,56 @@ class TCPTransport: NSObject, Transport, NetServiceBrowserDelegate, NetServiceDe
|
|||
}
|
||||
}
|
||||
|
||||
func manuallyConnect(withConnectionString: String) async throws {
|
||||
let hashedIdentifier = withConnectionString.toUUIDFormatHash() ?? UUID()
|
||||
let manualDevice = Device(id: hashedIdentifier,
|
||||
name: "\(withConnectionString) (Manual)",
|
||||
transportType: .tcp, identifier: withConnectionString)
|
||||
try await AccessoryManager.shared.connect(to: manualDevice)
|
||||
func device(forManualConnection connectionString: String) -> Device? {
|
||||
let parts = connectionString.split(separator: ":")
|
||||
var identifier: String
|
||||
|
||||
switch parts.count {
|
||||
case 1:
|
||||
// host & default port
|
||||
identifier = "\(parts[0]):4403"
|
||||
|
||||
case 2:
|
||||
// host & port
|
||||
if parts[1].isValidTCPPort {
|
||||
identifier = "\(parts[0]):\(parts[1])"
|
||||
}
|
||||
fallthrough
|
||||
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
let hashedIdentifier = identifier.toUUIDFormatHash()
|
||||
return Device(id: hashedIdentifier,
|
||||
name: "\(connectionString) (Manual)",
|
||||
transportType: .tcp,
|
||||
identifier: connectionString,
|
||||
isManualConnection: true)
|
||||
}
|
||||
|
||||
func manuallyConnect(toDevice device: Device) async throws {
|
||||
try await AccessoryManager.shared.connect(to: device)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
extension StringProtocol {
|
||||
var isValidTCPPort: Bool {
|
||||
// Check if the string is non-empty and contains only digits
|
||||
guard !isEmpty, allSatisfy({ $0.isNumber }) else {
|
||||
return false
|
||||
}
|
||||
|
||||
// Parse the string to an integer
|
||||
guard let port = Int(self) else {
|
||||
return false // Fails if the string can't be converted to an integer
|
||||
}
|
||||
|
||||
// Check if the port is in the valid TCP range (0–65535)
|
||||
return port >= 0 && port <= 65535
|
||||
}
|
||||
}
|
||||
|
||||
extension NetService {
|
||||
var ipv4Address: String? {
|
||||
for addressData in addresses ?? [] {
|
||||
|
|
|
|||
|
|
@ -23,4 +23,12 @@ extension Bundle {
|
|||
public var isTestFlight: Bool {
|
||||
return appStoreReceiptURL?.lastPathComponent == "sandboxReceipt"
|
||||
}
|
||||
|
||||
public var isDebug: Bool {
|
||||
#if DEBUG
|
||||
return true
|
||||
#else
|
||||
return false
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -79,6 +79,7 @@ extension UserDefaults {
|
|||
case showDeviceOnboarding
|
||||
case usageDataAndCrashReporting
|
||||
case autoconnectOnDiscovery
|
||||
case manualConnections
|
||||
case testIntEnum
|
||||
}
|
||||
|
||||
|
|
@ -178,6 +179,36 @@ extension UserDefaults {
|
|||
|
||||
@UserDefault(.testIntEnum, defaultValue: .one)
|
||||
static var testIntEnum: TestIntEnum
|
||||
|
||||
static var manualConnections: [Device] {
|
||||
get {
|
||||
// Retrieve data from UserDefaults
|
||||
guard let data = UserDefaults.standard.data(forKey: Keys.manualConnections.rawValue) else {
|
||||
return []
|
||||
}
|
||||
|
||||
// Decode the Data back to [Device]
|
||||
do {
|
||||
let decoder = JSONDecoder()
|
||||
let devices = try decoder.decode([Device].self, from: data)
|
||||
return devices
|
||||
} catch {
|
||||
return []
|
||||
}
|
||||
}
|
||||
set {
|
||||
do {
|
||||
// Encode the [Device] to Data
|
||||
let encoder = JSONEncoder()
|
||||
let data = try encoder.encode(newValue)
|
||||
|
||||
// Store the Data in UserDefaults
|
||||
UserDefaults.standard.set(data, forKey: Keys.manualConnections.rawValue)
|
||||
} catch {
|
||||
print("Failed to encode manualConnections: \(error)")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
enum TestIntEnum: Int, Decodable {
|
||||
|
|
|
|||
|
|
@ -26,8 +26,6 @@ struct Connect: View {
|
|||
@State var isUnsetRegion = false
|
||||
@State var invalidFirmwareVersion = false
|
||||
@State var liveActivityStarted = false
|
||||
@State var presentingSwitchPreferredPeripheral = false
|
||||
@State var selectedPeripherialId = ""
|
||||
|
||||
var body: some View {
|
||||
NavigationStack {
|
||||
|
|
@ -264,60 +262,24 @@ struct Connect: View {
|
|||
.textCase(nil)
|
||||
|
||||
if !(accessoryManager.isConnected || accessoryManager .isConnecting) {
|
||||
Section(header: HStack {
|
||||
Text("Available Radios").font(.title)
|
||||
Spacer()
|
||||
ManualConnectionMenu()
|
||||
}) {
|
||||
ForEach(accessoryManager.devices.sorted(by: { $0.name < $1.name })) { device in
|
||||
HStack {
|
||||
if UserDefaults.preferredPeripheralId == device.id.uuidString {
|
||||
Image(systemName: "star.fill")
|
||||
.imageScale(.large).foregroundColor(.yellow)
|
||||
.padding(.trailing)
|
||||
} else {
|
||||
Image(systemName: "circle.fill")
|
||||
.imageScale(.large).foregroundColor(.gray)
|
||||
.padding(.trailing)
|
||||
}
|
||||
VStack(alignment: .leading) {
|
||||
Button(action: {
|
||||
if UserDefaults.preferredPeripheralId.count > 0 && device.id.uuidString != UserDefaults.preferredPeripheralId {
|
||||
if accessoryManager.allowDisconnect {
|
||||
Task { try await accessoryManager.disconnect() }
|
||||
}
|
||||
presentingSwitchPreferredPeripheral = true
|
||||
selectedPeripherialId = device.id.uuidString
|
||||
} else {
|
||||
Task {
|
||||
try? await accessoryManager.connect(to: device)
|
||||
}
|
||||
}
|
||||
}) {
|
||||
Text(device.name).font(.callout)
|
||||
}
|
||||
// Show transport type
|
||||
TransportIcon(transportType: device.transportType)
|
||||
}
|
||||
Spacer()
|
||||
VStack {
|
||||
device.getSignalStrength().map { SignalStrengthIndicator(signalStrength: $0) }
|
||||
}
|
||||
}.padding([.bottom, .top])
|
||||
}
|
||||
}
|
||||
.confirmationDialog("Connecting to a new radio will clear all app data on the phone.", isPresented: $presentingSwitchPreferredPeripheral, titleVisibility: .visible) {
|
||||
Button("Connect to new radio?", role: .destructive) {
|
||||
UserDefaults.preferredPeripheralId = selectedPeripherialId
|
||||
UserDefaults.preferredPeripheralNum = 0
|
||||
if accessoryManager.allowDisconnect {
|
||||
Task { try await accessoryManager.disconnect() }
|
||||
Group {
|
||||
Section(header: HStack {
|
||||
Text("Available Radios").font(.title)
|
||||
Spacer()
|
||||
ManualConnectionMenu()
|
||||
}) {
|
||||
ForEach(accessoryManager.devices.sorted(by: { $0.name < $1.name })) { device in
|
||||
DeviceConnectRow(device: device)
|
||||
}
|
||||
clearCoreDataDatabase(context: context, includeRoutes: false)
|
||||
clearNotifications()
|
||||
if let radio = accessoryManager.devices.first(where: { $0.id.uuidString == selectedPeripherialId }) {
|
||||
Task {
|
||||
try await accessoryManager.connect(to: radio)
|
||||
}
|
||||
if UserDefaults.manualConnections.count > 0 {
|
||||
Section(header: Text("Manual Connections").font(.title)) {
|
||||
ForEach(UserDefaults.manualConnections) { device in
|
||||
DeviceConnectRow(device: device)
|
||||
}.onDelete { offsets in
|
||||
var list = UserDefaults.manualConnections
|
||||
list.remove(atOffsets: offsets)
|
||||
UserDefaults.manualConnections = list
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -472,6 +434,10 @@ struct TransportIcon: View {
|
|||
}
|
||||
|
||||
struct ManualConnectionMenu: View {
|
||||
|
||||
@EnvironmentObject var accessoryManager: AccessoryManager
|
||||
@Environment(\.managedObjectContext) var context
|
||||
|
||||
private struct IterableTransport: Identifiable {
|
||||
let id: UUID
|
||||
let icon: Image
|
||||
|
|
@ -490,7 +456,9 @@ struct ManualConnectionMenu: View {
|
|||
@State private var selectedTransport: IterableTransport?
|
||||
@State private var showAlert: Bool = false
|
||||
@State private var connectionString = ""
|
||||
|
||||
@State var presentingSwitchPreferredPeripheral = false
|
||||
@State var deviceForManualConnection: Device?
|
||||
|
||||
var body: some View {
|
||||
Menu {
|
||||
ForEach(transports) { transport in
|
||||
|
|
@ -520,11 +488,93 @@ struct ManualConnectionMenu: View {
|
|||
|
||||
Button("OK", action: {
|
||||
if !connectionString.isEmpty {
|
||||
Task {
|
||||
try await selectedTransport.transport.manuallyConnect(withConnectionString: connectionString)
|
||||
if let device = selectedTransport.transport.device(forManualConnection: connectionString) {
|
||||
if UserDefaults.preferredPeripheralId == device.id.uuidString {
|
||||
Task {
|
||||
try await selectedTransport.transport.manuallyConnect(toDevice: device)
|
||||
}
|
||||
} else {
|
||||
deviceForManualConnection = device
|
||||
presentingSwitchPreferredPeripheral = true
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}.confirmationDialog("Connecting to a new radio will clear all app data on the phone.", isPresented: $presentingSwitchPreferredPeripheral, titleVisibility: .visible) {
|
||||
Button("Connect to new radio?", role: .destructive) {
|
||||
if let device = deviceForManualConnection {
|
||||
UserDefaults.preferredPeripheralId = device.id.uuidString
|
||||
UserDefaults.preferredPeripheralNum = 0
|
||||
if accessoryManager.allowDisconnect {
|
||||
Task { try await accessoryManager.disconnect() }
|
||||
}
|
||||
clearCoreDataDatabase(context: context, includeRoutes: false)
|
||||
clearNotifications()
|
||||
Task {
|
||||
try await selectedTransport?.transport.manuallyConnect(toDevice: device)
|
||||
}
|
||||
|
||||
// Clean up just in case
|
||||
deviceForManualConnection = nil
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct DeviceConnectRow: View {
|
||||
@Environment(\.managedObjectContext) var context
|
||||
@EnvironmentObject var accessoryManager: AccessoryManager
|
||||
@State var presentingSwitchPreferredPeripheral = false
|
||||
var device: Device
|
||||
|
||||
var body: some View {
|
||||
HStack {
|
||||
if UserDefaults.preferredPeripheralId == device.id.uuidString {
|
||||
Image(systemName: "star.fill")
|
||||
.imageScale(.large).foregroundColor(.yellow)
|
||||
.padding(.trailing)
|
||||
} else {
|
||||
Image(systemName: "circle.fill")
|
||||
.imageScale(.large).foregroundColor(.gray)
|
||||
.padding(.trailing)
|
||||
}
|
||||
VStack(alignment: .leading) {
|
||||
Button(action: {
|
||||
if UserDefaults.preferredPeripheralId.count > 0 && device.id.uuidString != UserDefaults.preferredPeripheralId {
|
||||
if accessoryManager.allowDisconnect {
|
||||
Task { try await accessoryManager.disconnect() }
|
||||
}
|
||||
presentingSwitchPreferredPeripheral = true
|
||||
} else {
|
||||
Task {
|
||||
try? await accessoryManager.connect(to: device)
|
||||
}
|
||||
}
|
||||
}) {
|
||||
Text(device.name).font(.callout)
|
||||
}
|
||||
// Show transport type
|
||||
TransportIcon(transportType: device.transportType)
|
||||
}
|
||||
Spacer()
|
||||
VStack {
|
||||
device.getSignalStrength().map { SignalStrengthIndicator(signalStrength: $0) }
|
||||
}
|
||||
}.padding([.bottom, .top])
|
||||
.confirmationDialog("Connecting to a new radio will clear all app data on the phone.", isPresented: $presentingSwitchPreferredPeripheral, titleVisibility: .visible) {
|
||||
Button("Connect to new radio?", role: .destructive) {
|
||||
UserDefaults.preferredPeripheralId = device.id.uuidString
|
||||
UserDefaults.preferredPeripheralNum = 0
|
||||
if accessoryManager.allowDisconnect {
|
||||
Task { try await accessoryManager.disconnect() }
|
||||
}
|
||||
clearCoreDataDatabase(context: context, includeRoutes: false)
|
||||
clearNotifications()
|
||||
Task {
|
||||
try await accessoryManager.connect(to: device)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue