mirror of
https://github.com/meshtastic/Meshtastic-Apple.git
synced 2026-04-20 22:13:56 +00:00
AI guided fix for channels list issues
This commit is contained in:
parent
24a7270e50
commit
3ac9cd47e7
7 changed files with 442 additions and 491 deletions
|
|
@ -2,7 +2,6 @@
|
|||
"sourceLanguage" : "en",
|
||||
"strings" : {
|
||||
"" : {
|
||||
"shouldTranslate" : false,
|
||||
"localizations" : {
|
||||
"da" : {
|
||||
"stringUnit" : {
|
||||
|
|
@ -10,7 +9,8 @@
|
|||
"value" : ""
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"shouldTranslate" : false
|
||||
},
|
||||
"\t%@" : {
|
||||
"localizations" : {
|
||||
|
|
@ -225,95 +225,83 @@
|
|||
},
|
||||
"shouldTranslate" : false
|
||||
},
|
||||
" : %@" : {
|
||||
": %@" : {
|
||||
"localizations" : {
|
||||
"da" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : " : %@"
|
||||
}
|
||||
},
|
||||
"es" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : " : %@"
|
||||
"value" : ": %@"
|
||||
}
|
||||
},
|
||||
"it" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : " : %@"
|
||||
"value" : ": %@"
|
||||
}
|
||||
},
|
||||
"ja" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : " : %@"
|
||||
"value" : ": %@"
|
||||
}
|
||||
},
|
||||
"sr" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : " : %@"
|
||||
"value" : ": %@"
|
||||
}
|
||||
},
|
||||
"zh-Hans" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : " : %@"
|
||||
"value" : ": %@"
|
||||
}
|
||||
},
|
||||
"zh-Hant-TW" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : " : %@"
|
||||
"value" : ": %@"
|
||||
}
|
||||
}
|
||||
},
|
||||
"shouldTranslate" : false
|
||||
},
|
||||
" : %d" : {
|
||||
": %d" : {
|
||||
"localizations" : {
|
||||
"da" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : " : %d"
|
||||
}
|
||||
},
|
||||
"es" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : " : %d"
|
||||
"value" : ": %d"
|
||||
}
|
||||
},
|
||||
"it" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : " : %d"
|
||||
"value" : ": %d"
|
||||
}
|
||||
},
|
||||
"ja" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : " : %d"
|
||||
"value" : ": %d"
|
||||
}
|
||||
},
|
||||
"sr" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : " : %d"
|
||||
"value" : ": %d"
|
||||
}
|
||||
},
|
||||
"zh-Hans" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : " : %d"
|
||||
"value" : ": %d"
|
||||
}
|
||||
},
|
||||
"zh-Hant-TW" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : " : %d"
|
||||
"value" : ": %d"
|
||||
}
|
||||
}
|
||||
},
|
||||
|
|
@ -3018,7 +3006,9 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"A default self-signed certificate is included for localhost connections. Import a custom .p12 if needed. Client CA (.pem) validates connecting TAK clients." : {},
|
||||
"A default self-signed certificate is included for localhost connections. Import a custom .p12 if needed. Client CA (.pem) validates connecting TAK clients." : {
|
||||
|
||||
},
|
||||
"A green lock means the channel is securely encrypted with either a 128 or 256 bit AES key." : {
|
||||
"localizations" : {
|
||||
"es" : {
|
||||
|
|
@ -3863,7 +3853,9 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"Add CA" : {},
|
||||
"Add CA" : {
|
||||
|
||||
},
|
||||
"Add Channel" : {
|
||||
"localizations" : {
|
||||
"da" : {
|
||||
|
|
@ -11484,8 +11476,12 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"Client CA Certificate" : {},
|
||||
"Client Configuration" : {},
|
||||
"Client CA Certificate" : {
|
||||
|
||||
},
|
||||
"Client Configuration" : {
|
||||
|
||||
},
|
||||
"Client Hidden" : {
|
||||
"extractionState" : "stale",
|
||||
"localizations" : {
|
||||
|
|
@ -12186,7 +12182,9 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"Configuration" : {},
|
||||
"Configuration" : {
|
||||
|
||||
},
|
||||
"Configuration for: %@" : {
|
||||
"localizations" : {
|
||||
"da" : {
|
||||
|
|
@ -14570,7 +14568,9 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"Delete All" : {},
|
||||
"Delete All" : {
|
||||
|
||||
},
|
||||
"Delete all config, keys and BLE bonds? " : {
|
||||
"localizations" : {
|
||||
"es" : {
|
||||
|
|
@ -18174,7 +18174,9 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"Download TAK Server Data Package" : {},
|
||||
"Download TAK Server Data Package" : {
|
||||
|
||||
},
|
||||
"Drag & Drop Firmware Update" : {
|
||||
"localizations" : {
|
||||
"da" : {
|
||||
|
|
@ -18961,7 +18963,9 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"Enable TAK Server" : {},
|
||||
"Enable TAK Server" : {
|
||||
|
||||
},
|
||||
"Enable this device as a Store and Forward server. Requires an ESP32 device with PSRAM." : {
|
||||
"localizations" : {
|
||||
"da" : {
|
||||
|
|
@ -19728,8 +19732,12 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"Enter P12 Password" : {},
|
||||
"Enter the password for the PKCS#12 file" : {},
|
||||
"Enter P12 Password" : {
|
||||
|
||||
},
|
||||
"Enter the password for the PKCS#12 file" : {
|
||||
|
||||
},
|
||||
"environment" : {
|
||||
"extractionState" : "stale",
|
||||
"localizations" : {
|
||||
|
|
@ -23771,7 +23779,9 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"Generate a data package (.zip) to configure TAK clients to connect to this server." : {},
|
||||
"Generate a data package (.zip) to configure TAK clients to connect to this server." : {
|
||||
|
||||
},
|
||||
"Generate a new private key to replace the one currently in use. The public key will automatically be regenerated from your private key." : {
|
||||
"localizations" : {
|
||||
"es" : {
|
||||
|
|
@ -27266,10 +27276,18 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"Import" : {},
|
||||
"Import .pem" : {},
|
||||
"Import Custom .p12" : {},
|
||||
"Import Error" : {},
|
||||
"Import" : {
|
||||
|
||||
},
|
||||
"Import .pem" : {
|
||||
|
||||
},
|
||||
"Import Custom .p12" : {
|
||||
|
||||
},
|
||||
"Import Error" : {
|
||||
|
||||
},
|
||||
"Import Route" : {
|
||||
"localizations" : {
|
||||
"da" : {
|
||||
|
|
@ -32997,7 +33015,9 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"mTLS" : {},
|
||||
"mTLS" : {
|
||||
|
||||
},
|
||||
"Multiplier" : {
|
||||
"localizations" : {
|
||||
"da" : {
|
||||
|
|
@ -39159,7 +39179,9 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"Port" : {},
|
||||
"Port" : {
|
||||
|
||||
},
|
||||
"Position" : {
|
||||
"localizations" : {
|
||||
"da" : {
|
||||
|
|
@ -42816,7 +42838,9 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"Reload Bundled Certificates" : {},
|
||||
"Reload Bundled Certificates" : {
|
||||
|
||||
},
|
||||
"Remote administration for: %@" : {
|
||||
"localizations" : {
|
||||
"da" : {
|
||||
|
|
@ -43623,7 +43647,9 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"Reset to Default" : {},
|
||||
"Reset to Default" : {
|
||||
|
||||
},
|
||||
"Restart" : {
|
||||
"localizations" : {
|
||||
"da" : {
|
||||
|
|
@ -43676,7 +43702,9 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"Restart Server" : {},
|
||||
"Restart Server" : {
|
||||
|
||||
},
|
||||
"Restart to the node you are connected to" : {
|
||||
"localizations" : {
|
||||
"da" : {
|
||||
|
|
@ -46448,8 +46476,6 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"Secure mTLS connection on port 8089. Both server and client certificates are required." : {},
|
||||
|
||||
"Secure mTLS connection on port 8089. Both server and client certificates are required. TAK Channel Index selects the channel index where TAK messages will be sent." : {
|
||||
"comment" : "A footer for the TAK Server configuration section.",
|
||||
"isCommentAutoGenerated" : true
|
||||
|
|
@ -49143,7 +49169,9 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"Server Certificate" : {},
|
||||
"Server Certificate" : {
|
||||
|
||||
},
|
||||
"Server Option" : {
|
||||
"localizations" : {
|
||||
"da" : {
|
||||
|
|
@ -49190,7 +49218,9 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"Server Status" : {},
|
||||
"Server Status" : {
|
||||
|
||||
},
|
||||
"Set" : {
|
||||
"localizations" : {
|
||||
"da" : {
|
||||
|
|
@ -49237,6 +49267,10 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"Set a channel name" : {
|
||||
"comment" : "A label describing the action to set a channel name.",
|
||||
"isCommentAutoGenerated" : true
|
||||
},
|
||||
"Set LoRa Region" : {
|
||||
"localizations" : {
|
||||
"da" : {
|
||||
|
|
@ -49856,6 +49890,10 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"Share with TAK Buddies" : {
|
||||
"comment" : "A button that allows a user to share the QR code of their TAK-configured channel with other TAK users.",
|
||||
"isCommentAutoGenerated" : true
|
||||
},
|
||||
"Share your location in real-time and keep your group coordinated with integrated GPS features." : {
|
||||
"localizations" : {
|
||||
"de" : {
|
||||
|
|
@ -52018,7 +52056,9 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"Status" : {},
|
||||
"Status" : {
|
||||
|
||||
},
|
||||
"Stay Connected Anywhere" : {
|
||||
"localizations" : {
|
||||
"de" : {
|
||||
|
|
@ -52656,9 +52696,18 @@
|
|||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"TAK Cannot Be Used on Public Channel" : {
|
||||
"comment" : "A warning title that appears when the TAK Server is configured to use the public channel.",
|
||||
"isCommentAutoGenerated" : true
|
||||
},
|
||||
"TAK Channel Index" : {
|
||||
"comment" : "A label for the index of the TAK channel.",
|
||||
"isCommentAutoGenerated" : true
|
||||
},
|
||||
"TAK Server" : {
|
||||
|
||||
},
|
||||
"TAK Server" : {},
|
||||
"TAK Tracker" : {
|
||||
"extractionState" : "stale",
|
||||
"localizations" : {
|
||||
|
|
@ -55989,7 +56038,9 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"TLS Certificates" : {},
|
||||
"TLS Certificates" : {
|
||||
|
||||
},
|
||||
"TLS Enabled" : {
|
||||
"localizations" : {
|
||||
"da" : {
|
||||
|
|
@ -62889,88 +62940,6 @@
|
|||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
": %@" : {
|
||||
"localizations" : {
|
||||
"es" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : ": %@"
|
||||
}
|
||||
},
|
||||
"it" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : ": %@"
|
||||
}
|
||||
},
|
||||
"ja" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : ": %@"
|
||||
}
|
||||
},
|
||||
"sr" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : ": %@"
|
||||
}
|
||||
},
|
||||
"zh-Hans" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : ": %@"
|
||||
}
|
||||
},
|
||||
"zh-Hant-TW" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : ": %@"
|
||||
}
|
||||
}
|
||||
},
|
||||
"shouldTranslate" : false
|
||||
},
|
||||
": %d" : {
|
||||
"localizations" : {
|
||||
"es" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : ": %d"
|
||||
}
|
||||
},
|
||||
"it" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : ": %d"
|
||||
}
|
||||
},
|
||||
"ja" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : ": %d"
|
||||
}
|
||||
},
|
||||
"sr" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : ": %d"
|
||||
}
|
||||
},
|
||||
"zh-Hans" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : ": %d"
|
||||
}
|
||||
},
|
||||
"zh-Hant-TW" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : ": %d"
|
||||
}
|
||||
}
|
||||
},
|
||||
"shouldTranslate" : false
|
||||
}
|
||||
},
|
||||
"version" : "1.1"
|
||||
|
|
|
|||
|
|
@ -54,6 +54,8 @@ extension ChannelEntity {
|
|||
channel.settings.name = self.name ?? ""
|
||||
channel.settings.psk = self.psk ?? Data()
|
||||
channel.role = Channel.Role(rawValue: Int(self.role)) ?? Channel.Role.secondary
|
||||
channel.settings.uplinkEnabled = self.uplinkEnabled
|
||||
channel.settings.downlinkEnabled = self.downlinkEnabled
|
||||
channel.settings.moduleSettings.positionPrecision = UInt32(self.positionPrecision)
|
||||
channel.settings.moduleSettings.isMuted = self.mute
|
||||
return channel
|
||||
|
|
|
|||
|
|
@ -179,53 +179,58 @@ actor MeshPackets {
|
|||
}
|
||||
|
||||
nonisolated private func channelPacket (channel: Channel, fromNum: Int64, context: NSManagedObjectContext) {
|
||||
if channel.isInitialized && channel.hasSettings && channel.role != Channel.Role.disabled {
|
||||
let logString = String.localizedStringWithFormat("mesh.log.channel.received %d %@".localized, channel.index, String(fromNum))
|
||||
Logger.mesh.info("🎛️ \(logString, privacy: .public)")
|
||||
|
||||
let fetchedMyInfoRequest = MyInfoEntity.fetchRequest()
|
||||
fetchedMyInfoRequest.predicate = NSPredicate(format: "myNodeNum == %lld", fromNum)
|
||||
|
||||
do {
|
||||
let fetchedMyInfo = try context.fetch(fetchedMyInfoRequest)
|
||||
if fetchedMyInfo.count == 1 {
|
||||
let newChannel = ChannelEntity(context: context)
|
||||
newChannel.id = Int32(channel.index)
|
||||
newChannel.index = Int32(channel.index)
|
||||
newChannel.uplinkEnabled = channel.settings.uplinkEnabled
|
||||
newChannel.downlinkEnabled = channel.settings.downlinkEnabled
|
||||
newChannel.name = channel.settings.name
|
||||
newChannel.role = Int32(channel.role.rawValue)
|
||||
newChannel.psk = channel.settings.psk
|
||||
if channel.settings.hasModuleSettings {
|
||||
newChannel.positionPrecision = Int32(truncatingIfNeeded: channel.settings.moduleSettings.positionPrecision)
|
||||
newChannel.mute = channel.settings.moduleSettings.isMuted
|
||||
}
|
||||
guard let mutableChannels = fetchedMyInfo[0].channels!.mutableCopy() as? NSMutableOrderedSet else {
|
||||
return
|
||||
}
|
||||
if let oldChannel = mutableChannels.first(where: {($0 as AnyObject).index == newChannel.index }) as? ChannelEntity {
|
||||
let index = mutableChannels.index(of: oldChannel as Any)
|
||||
mutableChannels.replaceObject(at: index, with: newChannel)
|
||||
} else {
|
||||
mutableChannels.add(newChannel)
|
||||
}
|
||||
fetchedMyInfo[0].channels = mutableChannels.copy() as? NSOrderedSet
|
||||
context.refresh(newChannel, mergeChanges: true)
|
||||
do {
|
||||
try context.save()
|
||||
} catch {
|
||||
Logger.data.error("💥 Failed to save channel: \(error.localizedDescription, privacy: .public)")
|
||||
}
|
||||
Logger.data.info("💾 Updated MyInfo channel \(channel.index, privacy: .public) from Channel App Packet For: \(fetchedMyInfo[0].myNodeNum, privacy: .public)")
|
||||
} else if channel.role.rawValue > 0 {
|
||||
Logger.data.error("💥Trying to save a channel to a MyInfo that does not exist: \(fromNum.toHex(), privacy: .public)")
|
||||
}
|
||||
} catch {
|
||||
context.rollback()
|
||||
let nsError = error as NSError
|
||||
Logger.data.error("💥 Error Saving MyInfo Channel from ADMIN_APP \(nsError, privacy: .public)")
|
||||
guard channel.isInitialized && channel.hasSettings && channel.role != Channel.Role.disabled else { return }
|
||||
let logString = String.localizedStringWithFormat("mesh.log.channel.received %d %@".localized, channel.index, String(fromNum))
|
||||
Logger.mesh.info("🎛️ \(logString, privacy: .public)")
|
||||
|
||||
let fetchedMyInfoRequest = MyInfoEntity.fetchRequest()
|
||||
fetchedMyInfoRequest.predicate = NSPredicate(format: "myNodeNum == %lld", fromNum)
|
||||
fetchedMyInfoRequest.fetchLimit = 1
|
||||
|
||||
do {
|
||||
let fetchedMyInfo = try context.fetch(fetchedMyInfoRequest)
|
||||
guard let myInfo = fetchedMyInfo.first else {
|
||||
Logger.data.error("💥 Trying to save a channel to a MyInfo that does not exist: \(fromNum.toHex(), privacy: .public)")
|
||||
return
|
||||
}
|
||||
|
||||
// Fetch by index alone (the uniqueness key) so we always find an existing entity
|
||||
// regardless of whether myInfoChannel was previously corrupted to nil.
|
||||
// Never insert a new entity that would conflict with an existing one — the resulting
|
||||
// NSManagedObjectContextDidSave notification would carry the pre-resolution nil value
|
||||
// for myInfoChannel, making the channel vanish from any @FetchRequest predicated on it.
|
||||
let channelFetch = ChannelEntity.fetchRequest()
|
||||
channelFetch.predicate = NSPredicate(format: "index == %d", Int(channel.index))
|
||||
channelFetch.fetchLimit = 1
|
||||
|
||||
let channelEntity: ChannelEntity
|
||||
if let existing = try context.fetch(channelFetch).first {
|
||||
channelEntity = existing
|
||||
} else {
|
||||
channelEntity = ChannelEntity(context: context)
|
||||
channelEntity.id = Int32(channel.index)
|
||||
channelEntity.index = Int32(channel.index)
|
||||
}
|
||||
|
||||
// Always (re-)establish the relationship — a no-op if already correct,
|
||||
// but repairs channels whose myInfoChannel was nullified by the old code.
|
||||
channelEntity.myInfoChannel = myInfo
|
||||
channelEntity.uplinkEnabled = channel.settings.uplinkEnabled
|
||||
channelEntity.downlinkEnabled = channel.settings.downlinkEnabled
|
||||
channelEntity.name = channel.settings.name
|
||||
channelEntity.role = Int32(channel.role.rawValue)
|
||||
channelEntity.psk = channel.settings.psk
|
||||
if channel.settings.hasModuleSettings {
|
||||
channelEntity.positionPrecision = Int32(truncatingIfNeeded: channel.settings.moduleSettings.positionPrecision)
|
||||
channelEntity.mute = channel.settings.moduleSettings.isMuted
|
||||
}
|
||||
|
||||
try context.save()
|
||||
Logger.data.info("💾 Updated MyInfo channel \(channel.index, privacy: .public) from Channel App Packet For: \(myInfo.myNodeNum, privacy: .public)")
|
||||
} catch {
|
||||
context.rollback()
|
||||
let nsError = error as NSError
|
||||
Logger.data.error("💥 Error Saving MyInfo Channel from ADMIN_APP \(nsError, privacy: .public)")
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -22,11 +22,26 @@ struct ChannelList: View {
|
|||
|
||||
var restrictedChannels = ["gpio", "mqtt", "serial", "admin"]
|
||||
|
||||
@FetchRequest(
|
||||
@FetchRequest private var channels: FetchedResults<ChannelEntity>
|
||||
|
||||
init(node: Binding<NodeInfoEntity?>, channelSelection: Binding<ChannelEntity?>) {
|
||||
_node = node
|
||||
_channelSelection = channelSelection
|
||||
let predicate: NSPredicate
|
||||
if let nodeNum = node.wrappedValue?.num {
|
||||
predicate = NSCompoundPredicate(andPredicateWithSubpredicates: [
|
||||
NSPredicate(format: "myInfoChannel.myNodeNum == %lld", nodeNum),
|
||||
NSPredicate(format: "role > 0")
|
||||
])
|
||||
} else {
|
||||
predicate = NSPredicate(value: false)
|
||||
}
|
||||
_channels = FetchRequest(
|
||||
sortDescriptors: [NSSortDescriptor(keyPath: \ChannelEntity.index, ascending: true)],
|
||||
predicate: nil,
|
||||
predicate: predicate,
|
||||
animation: .default
|
||||
) private var channels: FetchedResults<ChannelEntity>
|
||||
)
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
private func makeChannelRow(
|
||||
|
|
|
|||
|
|
@ -28,35 +28,23 @@ struct Channels: View {
|
|||
@Environment(\.sizeCategory) var sizeCategory
|
||||
@Environment(\.colorScheme) private var colorScheme
|
||||
|
||||
var node: NodeInfoEntity?
|
||||
var node: NodeInfoEntity
|
||||
|
||||
@State var hasChanges = false
|
||||
@State var hasValidKey = true
|
||||
@State private var isPresentingSaveConfirm: Bool = false
|
||||
@State var channelIndex: Int32 = 0
|
||||
@State var channelName = ""
|
||||
@State var channelKeySize = 16
|
||||
@State var channelKey = "AQ=="
|
||||
@State var channelRole = 0
|
||||
@State var uplink = false
|
||||
@State var downlink = false
|
||||
@State var positionPrecision = 32.0
|
||||
@State var preciseLocation = true
|
||||
@State var positionsEnabled = true
|
||||
@State var supportedVersion = true
|
||||
@State var selectedChannel: ChannelEntity?
|
||||
|
||||
/// Minimum Version for granular position configuration
|
||||
@State var minimumVersion = "2.2.24"
|
||||
@State private var isEditingChannel = false
|
||||
@State private var selectedChannel: ChannelEntity?
|
||||
@State private var editContext: NSManagedObjectContext?
|
||||
@State private var showingHelp = false
|
||||
|
||||
@FetchRequest(
|
||||
sortDescriptors: [NSSortDescriptor(key: "favorite", ascending: false),
|
||||
NSSortDescriptor(key: "lastHeard", ascending: false),
|
||||
NSSortDescriptor(key: "user.longName", ascending: true)],
|
||||
animation: .default)
|
||||
@FetchRequest private var channels: FetchedResults<ChannelEntity>
|
||||
|
||||
var nodes: FetchedResults<NodeInfoEntity>
|
||||
init(node: NodeInfoEntity) {
|
||||
self.node = node
|
||||
_channels = FetchRequest(
|
||||
sortDescriptors: [NSSortDescriptor(keyPath: \ChannelEntity.index, ascending: true)],
|
||||
predicate: NSPredicate(format: "myInfoChannel.myNodeNum == %lld", node.num),
|
||||
animation: .default
|
||||
)
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
|
||||
|
|
@ -65,79 +53,24 @@ struct Channels: View {
|
|||
TipView(CreateChannelsTip(), arrowEdge: .bottom)
|
||||
.tipBackground(colorScheme == .dark ? Color(.systemBackground) : Color(.secondarySystemBackground))
|
||||
.listRowSeparator(.hidden)
|
||||
if node != nil && node?.myInfo != nil {
|
||||
ForEach(node?.myInfo?.channels?.array as? [ChannelEntity] ?? [], id: \.self) { (channel: ChannelEntity) in
|
||||
Button(action: {
|
||||
channelIndex = channel.index
|
||||
channelRole = Int(channel.role)
|
||||
channelKey = channel.psk?.base64EncodedString() ?? ""
|
||||
if channelKey.count == 0 {
|
||||
channelKeySize = 0
|
||||
} else if channelKey == "AQ==" {
|
||||
channelKeySize = -1
|
||||
} else if channelKey.count == 4 {
|
||||
channelKeySize = 1
|
||||
} else if channelKey.count == 24 {
|
||||
channelKeySize = 16
|
||||
} else if channelKey.count == 32 {
|
||||
channelKeySize = 24
|
||||
} else if channelKey.count == 44 {
|
||||
channelKeySize = 32
|
||||
}
|
||||
channelName = channel.name ?? ""
|
||||
uplink = channel.uplinkEnabled
|
||||
downlink = channel.downlinkEnabled
|
||||
positionPrecision = Double(channel.positionPrecision)
|
||||
if !supportedVersion && channelRole == 1 {
|
||||
positionPrecision = 32
|
||||
preciseLocation = true
|
||||
positionsEnabled = true
|
||||
if channelKey == "AQ==" {
|
||||
positionPrecision = 14
|
||||
preciseLocation = false
|
||||
}
|
||||
} else if !supportedVersion && channelRole == 2 {
|
||||
positionPrecision = 0
|
||||
preciseLocation = false
|
||||
positionsEnabled = false
|
||||
} else {
|
||||
if channelKey == "AQ==" {
|
||||
preciseLocation = false
|
||||
if (positionPrecision > 0 && positionPrecision < 11) || positionPrecision > 14 {
|
||||
positionPrecision = 14
|
||||
}
|
||||
} else if positionPrecision == 32 {
|
||||
preciseLocation = true
|
||||
positionsEnabled = true
|
||||
} else {
|
||||
preciseLocation = false
|
||||
}
|
||||
if positionPrecision == 0 {
|
||||
positionsEnabled = false
|
||||
} else {
|
||||
positionsEnabled = true
|
||||
}
|
||||
}
|
||||
hasChanges = false
|
||||
selectedChannel = channel
|
||||
}) {
|
||||
VStack(alignment: .leading) {
|
||||
HStack {
|
||||
CircleText(text: String(channel.index), color: .accentColor, circleSize: 45)
|
||||
.padding(.trailing, 5)
|
||||
.brightness(0.1)
|
||||
VStack {
|
||||
HStack {
|
||||
ChannelLock(channel: channel)
|
||||
if channel.name?.isEmpty ?? false {
|
||||
if channel.role == 1 {
|
||||
Text(String("PrimaryChannel").camelCaseToWords()).font(.headline)
|
||||
} else {
|
||||
Text(String("Channel \(channel.index)").camelCaseToWords()).font(.headline)
|
||||
}
|
||||
ForEach(channels) { channel in
|
||||
Button(action: { beginEditing(channel: channel) }) {
|
||||
VStack(alignment: .leading) {
|
||||
HStack {
|
||||
CircleText(text: String(channel.index), color: .accentColor, circleSize: 45)
|
||||
.padding(.trailing, 5)
|
||||
.brightness(0.1)
|
||||
VStack {
|
||||
HStack {
|
||||
ChannelLock(channel: channel)
|
||||
if channel.name?.isEmpty ?? true {
|
||||
if channel.role == 1 {
|
||||
Text(String("PrimaryChannel").camelCaseToWords()).font(.headline)
|
||||
} else {
|
||||
Text(String(channel.name ?? "Channel \(channel.index)").camelCaseToWords()).font(.headline)
|
||||
Text(String("Channel \(channel.index)").camelCaseToWords()).font(.headline)
|
||||
}
|
||||
} else {
|
||||
Text(String(channel.name ?? "Channel \(channel.index)").camelCaseToWords()).font(.headline)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -146,89 +79,24 @@ struct Channels: View {
|
|||
}
|
||||
}
|
||||
}
|
||||
.sheet(item: $selectedChannel) { _ in
|
||||
.sheet(isPresented: $isEditingChannel, onDismiss: cancelEditing) {
|
||||
#if targetEnvironment(macCatalyst)
|
||||
Text("Channel")
|
||||
.font(.largeTitle)
|
||||
.padding()
|
||||
#endif
|
||||
ChannelForm(channelIndex: $channelIndex, channelName: $channelName, channelKeySize: $channelKeySize, channelKey: $channelKey, channelRole: $channelRole, uplink: $uplink, downlink: $downlink, positionPrecision: $positionPrecision, preciseLocation: $preciseLocation, positionsEnabled: $positionsEnabled, hasChanges: $hasChanges, hasValidKey: $hasValidKey, supportedVersion: $supportedVersion)
|
||||
.presentationDetents([.large])
|
||||
.presentationDragIndicator(.visible)
|
||||
.onFirstAppear {
|
||||
supportedVersion = accessoryManager.checkIsVersionSupported(forVersion: minimumVersion)
|
||||
if let channel = selectedChannel {
|
||||
ChannelForm(channel: channel)
|
||||
.presentationDetents([.large])
|
||||
.presentationDragIndicator(.visible)
|
||||
}
|
||||
HStack {
|
||||
Button {
|
||||
var channel = Channel()
|
||||
channel.index = channelIndex
|
||||
channel.role = ChannelRoles(rawValue: channelRole)?.protoEnumValue() ?? .secondary
|
||||
channel.index = channelIndex
|
||||
channel.settings.name = channelName
|
||||
channel.settings.psk = Data(base64Encoded: channelKey) ?? Data()
|
||||
channel.settings.uplinkEnabled = uplink
|
||||
channel.settings.downlinkEnabled = downlink
|
||||
channel.settings.moduleSettings.positionPrecision = UInt32(positionPrecision)
|
||||
selectedChannel!.role = Int32(channelRole)
|
||||
selectedChannel!.index = channelIndex
|
||||
selectedChannel!.name = channelName
|
||||
selectedChannel!.psk = Data(base64Encoded: channelKey) ?? Data()
|
||||
selectedChannel!.uplinkEnabled = uplink
|
||||
selectedChannel!.downlinkEnabled = downlink
|
||||
selectedChannel!.positionPrecision = Int32(positionPrecision)
|
||||
|
||||
guard let mutableChannels = node?.myInfo?.channels?.mutableCopy() as? NSMutableOrderedSet else {
|
||||
return
|
||||
}
|
||||
if mutableChannels.contains(selectedChannel as Any) {
|
||||
let replaceChannel = mutableChannels.first(where: { selectedChannel?.psk == ($0 as AnyObject).psk && selectedChannel?.name == ($0 as AnyObject).name})
|
||||
mutableChannels.replaceObject(at: mutableChannels.index(of: replaceChannel as Any), with: selectedChannel as Any)
|
||||
} else {
|
||||
mutableChannels.add(selectedChannel as Any)
|
||||
}
|
||||
node?.myInfo?.channels = mutableChannels.copy() as? NSOrderedSet
|
||||
context.refresh(selectedChannel!, mergeChanges: true)
|
||||
if channel.role != Channel.Role.disabled {
|
||||
do {
|
||||
try context.save()
|
||||
Logger.data.info("💾 Saved Channel: \(channel.settings.name, privacy: .public)")
|
||||
} catch {
|
||||
context.rollback()
|
||||
let nsError = error as NSError
|
||||
Logger.data.error("Unresolved Core Data error in the channel editor. Error: \(nsError, privacy: .public)")
|
||||
}
|
||||
} else {
|
||||
let objects = selectedChannel?.allPrivateMessages ?? []
|
||||
for object in objects {
|
||||
context.delete(object)
|
||||
}
|
||||
for node in nodes where node.channel == channel.index {
|
||||
context.delete(node)
|
||||
}
|
||||
context.delete(selectedChannel!)
|
||||
do {
|
||||
try context.save()
|
||||
Logger.data.info("💾 Deleted Channel: \(channel.settings.name, privacy: .public)")
|
||||
} catch {
|
||||
context.rollback()
|
||||
let nsError = error as NSError
|
||||
Logger.data.error("Unresolved Core Data error in the channel editor. Error: \(nsError, privacy: .public)")
|
||||
}
|
||||
}
|
||||
Task {
|
||||
_ = try await accessoryManager.saveChannel(channel: channel, fromUser: node!.user!, toUser: node!.user!)
|
||||
Task { @MainActor in
|
||||
selectedChannel = nil
|
||||
channelName = ""
|
||||
channelRole = 2
|
||||
hasChanges = false
|
||||
}
|
||||
accessoryManager.mqttManager.connectFromConfigSettings(node: node!)
|
||||
}
|
||||
saveChannel()
|
||||
} label: {
|
||||
Label("Save", systemImage: "square.and.arrow.down")
|
||||
}
|
||||
.disabled(!accessoryManager.isConnected)// || !hasChanges)// !hasValidKey)
|
||||
.disabled(!accessoryManager.isConnected)
|
||||
.buttonStyle(.bordered)
|
||||
.buttonBorderShape(.capsule)
|
||||
.controlSize(.large)
|
||||
|
|
@ -246,37 +114,9 @@ struct Channels: View {
|
|||
#endif
|
||||
}
|
||||
}
|
||||
if node?.myInfo?.channels?.array.count ?? 0 < 8 && node != nil {
|
||||
|
||||
if channels.count < 8 {
|
||||
Button {
|
||||
let channelIndexes = node?.myInfo?.channels?.compactMap({(ch) -> Int in
|
||||
return (ch as AnyObject).index
|
||||
})
|
||||
let firstChannelIndex = firstMissingChannelIndex(channelIndexes ?? [])
|
||||
channelKeySize = 16
|
||||
let key = generateChannelKey(size: channelKeySize)
|
||||
channelName = ""
|
||||
channelIndex = Int32(firstChannelIndex)
|
||||
channelRole = 2
|
||||
channelKey = key
|
||||
positionsEnabled = false
|
||||
preciseLocation = false
|
||||
positionPrecision = 0
|
||||
uplink = false
|
||||
downlink = false
|
||||
|
||||
let newChannel = ChannelEntity(context: context)
|
||||
newChannel.id = channelIndex
|
||||
newChannel.index = channelIndex
|
||||
newChannel.uplinkEnabled = uplink
|
||||
newChannel.downlinkEnabled = downlink
|
||||
newChannel.name = channelName
|
||||
newChannel.role = Int32(channelRole)
|
||||
newChannel.psk = Data(base64Encoded: channelKey) ?? Data()
|
||||
newChannel.positionPrecision = Int32(positionPrecision)
|
||||
selectedChannel = newChannel
|
||||
hasChanges = true
|
||||
|
||||
addChannel()
|
||||
} label: {
|
||||
Label("Add Channel", systemImage: "plus.square")
|
||||
}
|
||||
|
|
@ -315,6 +155,102 @@ struct Channels: View {
|
|||
ConnectedDevice(deviceConnected: accessoryManager.isConnected, name: accessoryManager.activeConnection?.device.shortName ?? "?")
|
||||
})
|
||||
}
|
||||
|
||||
// MARK: - Editing helpers
|
||||
|
||||
private func beginEditing(channel: ChannelEntity) {
|
||||
let childContext = NSManagedObjectContext(concurrencyType: .mainQueueConcurrencyType)
|
||||
childContext.parent = context
|
||||
guard let channelInChild = childContext.object(with: channel.objectID) as? ChannelEntity else { return }
|
||||
editContext = childContext
|
||||
selectedChannel = channelInChild
|
||||
isEditingChannel = true
|
||||
}
|
||||
|
||||
private func addChannel() {
|
||||
let channelIndexes = channels.map { Int($0.index) }
|
||||
let nextIndex = firstMissingChannelIndex(channelIndexes)
|
||||
let key = generateChannelKey(size: 16)
|
||||
|
||||
let childContext = NSManagedObjectContext(concurrencyType: .mainQueueConcurrencyType)
|
||||
childContext.parent = context
|
||||
|
||||
let newChannel = ChannelEntity(context: childContext)
|
||||
newChannel.id = Int32(nextIndex)
|
||||
newChannel.index = Int32(nextIndex)
|
||||
newChannel.role = 2 // Secondary
|
||||
newChannel.name = ""
|
||||
newChannel.psk = Data(base64Encoded: key)
|
||||
newChannel.uplinkEnabled = false
|
||||
newChannel.downlinkEnabled = false
|
||||
newChannel.positionPrecision = 0
|
||||
|
||||
if let myInfo = node.myInfo,
|
||||
let myInfoInChild = childContext.object(with: myInfo.objectID) as? MyInfoEntity {
|
||||
newChannel.myInfoChannel = myInfoInChild
|
||||
}
|
||||
|
||||
editContext = childContext
|
||||
selectedChannel = newChannel
|
||||
isEditingChannel = true
|
||||
}
|
||||
|
||||
private func saveChannel() {
|
||||
guard let editCtx = editContext, let channel = selectedChannel else { return }
|
||||
|
||||
let isNew = channel.objectID.isTemporaryID
|
||||
let channelIndex = channel.index
|
||||
let proto = channel.protoBuf
|
||||
|
||||
if channel.role == 0 { // Disabled = delete existing channel
|
||||
if !isNew, let parentChannel = context.object(with: channel.objectID) as? ChannelEntity {
|
||||
for message in parentChannel.allPrivateMessages {
|
||||
context.delete(message)
|
||||
}
|
||||
let nodesFetch = NodeInfoEntity.fetchRequest()
|
||||
nodesFetch.predicate = NSPredicate(format: "channel == %d", channelIndex)
|
||||
let orphans = (try? context.fetch(nodesFetch)) ?? []
|
||||
for orphan in orphans {
|
||||
context.delete(orphan)
|
||||
}
|
||||
context.delete(parentChannel)
|
||||
do {
|
||||
try context.save()
|
||||
Logger.data.info("💾 Deleted Channel \(channelIndex)")
|
||||
} catch {
|
||||
context.rollback()
|
||||
let nsError = error as NSError
|
||||
Logger.data.error("Unresolved CoreData error deleting channel. Error: \(nsError, privacy: .public)")
|
||||
}
|
||||
}
|
||||
isEditingChannel = false
|
||||
return
|
||||
}
|
||||
|
||||
do {
|
||||
try editCtx.save()
|
||||
try context.save()
|
||||
Logger.data.info("💾 Saved Channel: \(proto.settings.name, privacy: .public)")
|
||||
} catch {
|
||||
editCtx.rollback()
|
||||
let nsError = error as NSError
|
||||
Logger.data.error("Unresolved CoreData error saving channel. Error: \(nsError, privacy: .public)")
|
||||
return
|
||||
}
|
||||
|
||||
Task {
|
||||
_ = try? await accessoryManager.saveChannel(channel: proto, fromUser: node.user!, toUser: node.user!)
|
||||
Task { @MainActor in
|
||||
isEditingChannel = false
|
||||
}
|
||||
accessoryManager.mqttManager.connectFromConfigSettings(node: node)
|
||||
}
|
||||
}
|
||||
|
||||
private func cancelEditing() {
|
||||
selectedChannel = nil
|
||||
editContext = nil
|
||||
}
|
||||
}
|
||||
|
||||
func firstMissingChannelIndex(_ indexes: [Int]) -> Int {
|
||||
|
|
|
|||
|
|
@ -10,19 +10,18 @@ import MapKit
|
|||
|
||||
struct ChannelForm: View {
|
||||
|
||||
@Binding var channelIndex: Int32
|
||||
@Binding var channelName: String
|
||||
@Binding var channelKeySize: Int
|
||||
@Binding var channelKey: String
|
||||
@Binding var channelRole: Int
|
||||
@Binding var uplink: Bool
|
||||
@Binding var downlink: Bool
|
||||
@Binding var positionPrecision: Double
|
||||
@Binding var preciseLocation: Bool
|
||||
@Binding var positionsEnabled: Bool
|
||||
@Binding var hasChanges: Bool
|
||||
@Binding var hasValidKey: Bool
|
||||
@Binding var supportedVersion: Bool
|
||||
@ObservedObject var channel: ChannelEntity
|
||||
@EnvironmentObject var accessoryManager: AccessoryManager
|
||||
|
||||
/// UI-only state derived from channel data on appear
|
||||
@State private var channelKey = ""
|
||||
@State private var channelKeySize = 16
|
||||
@State private var positionsEnabled = true
|
||||
@State private var preciseLocation = true
|
||||
@State private var hasValidKey = true
|
||||
@State private var supportedVersion = true
|
||||
|
||||
private let minimumVersion = "2.2.24"
|
||||
|
||||
var body: some View {
|
||||
NavigationStack {
|
||||
|
|
@ -33,20 +32,18 @@ struct ChannelForm: View {
|
|||
Spacer()
|
||||
TextField(
|
||||
"Channel Name",
|
||||
text: $channelName
|
||||
text: Binding(
|
||||
get: { channel.name ?? "" },
|
||||
set: { channel.name = $0 }
|
||||
)
|
||||
)
|
||||
.disableAutocorrection(true)
|
||||
.keyboardType(.alphabet)
|
||||
.foregroundColor(Color.gray)
|
||||
.onChange(of: channelName) {
|
||||
channelName = channelName.replacing(" ", with: "")
|
||||
var totalBytes = channelName.utf8.count
|
||||
// Only mess with the value if it is too big
|
||||
while totalBytes > 11 {
|
||||
channelName = String(channelName.dropLast())
|
||||
totalBytes = channelName.utf8.count
|
||||
}
|
||||
hasChanges = true
|
||||
.onChange(of: channel.name) { _, name in
|
||||
var trimmed = (name ?? "").replacingOccurrences(of: " ", with: "")
|
||||
while trimmed.utf8.count > 11 { trimmed = String(trimmed.dropLast()) }
|
||||
if trimmed != name { channel.name = trimmed }
|
||||
}
|
||||
}
|
||||
HStack {
|
||||
|
|
@ -62,10 +59,12 @@ struct ChannelForm: View {
|
|||
Button {
|
||||
if channelKeySize == -1 {
|
||||
channelKey = "AQ=="
|
||||
} else if channelKeySize > 0 {
|
||||
channelKey = generateChannelKey(size: channelKeySize)
|
||||
} else {
|
||||
let key = generateChannelKey(size: channelKeySize)
|
||||
channelKey = key
|
||||
channelKey = ""
|
||||
}
|
||||
channel.psk = Data(base64Encoded: channelKey)
|
||||
} label: {
|
||||
Image(systemName: "lock.rotation")
|
||||
.font(.title)
|
||||
|
|
@ -90,27 +89,22 @@ struct ChannelForm: View {
|
|||
.background(
|
||||
RoundedRectangle(cornerRadius: 10.0)
|
||||
.stroke(
|
||||
hasValidKey ?
|
||||
Color.clear :
|
||||
Color.red
|
||||
, lineWidth: 2.0)
|
||||
|
||||
hasValidKey ? Color.clear : Color.red,
|
||||
lineWidth: 2.0)
|
||||
)
|
||||
.onChange(of: channelKey) {
|
||||
|
||||
let tempKey = Data(base64Encoded: channelKey) ?? Data()
|
||||
if tempKey.count == channelKeySize || channelKeySize == -1 {
|
||||
hasValidKey = true
|
||||
} else {
|
||||
hasValidKey = false
|
||||
}
|
||||
hasChanges = true
|
||||
.onChange(of: channelKey) { _, key in
|
||||
let data = Data(base64Encoded: key) ?? Data()
|
||||
hasValidKey = data.count == channelKeySize || channelKeySize == -1
|
||||
channel.psk = data.isEmpty ? nil : data
|
||||
}
|
||||
.disabled(channelKeySize <= 0)
|
||||
}
|
||||
HStack {
|
||||
if channelRole == 1 {
|
||||
Picker("Channel Role", selection: $channelRole) {
|
||||
if channel.role == 1 {
|
||||
Picker("Channel Role", selection: Binding(
|
||||
get: { Int(channel.role) },
|
||||
set: { channel.role = Int32($0) }
|
||||
)) {
|
||||
Text("Primary").tag(1)
|
||||
}
|
||||
.pickerStyle(.automatic)
|
||||
|
|
@ -118,7 +112,10 @@ struct ChannelForm: View {
|
|||
} else {
|
||||
Text("Channel Role")
|
||||
Spacer()
|
||||
Picker("Channel Role", selection: $channelRole) {
|
||||
Picker("Channel Role", selection: Binding(
|
||||
get: { Int(channel.role) },
|
||||
set: { channel.role = Int32($0) }
|
||||
)) {
|
||||
Text("Disabled").tag(0)
|
||||
Text("Secondary").tag(2)
|
||||
}
|
||||
|
|
@ -130,14 +127,17 @@ struct ChannelForm: View {
|
|||
Section(header: Text("Position")) {
|
||||
VStack(alignment: .leading) {
|
||||
Toggle(isOn: $positionsEnabled) {
|
||||
Label(channelRole == 1 ? "Positions Enabled" : "Allow Position Requests", systemImage: positionsEnabled ? "mappin" : "mappin.slash")
|
||||
Label(
|
||||
channel.role == 1 ? "Positions Enabled" : "Allow Position Requests",
|
||||
systemImage: positionsEnabled ? "mappin" : "mappin.slash"
|
||||
)
|
||||
}
|
||||
.toggleStyle(SwitchToggleStyle(tint: .accentColor))
|
||||
.disabled(!supportedVersion)
|
||||
}
|
||||
|
||||
if positionsEnabled {
|
||||
if (channelKey != "AQ==" && channelKeySize > 1) && channelRole > 0 {
|
||||
if channelKey != "AQ==" && channelKeySize > 1 && channel.role > 0 {
|
||||
VStack(alignment: .leading) {
|
||||
Toggle(isOn: $preciseLocation) {
|
||||
Label("Precise Location", systemImage: "scope")
|
||||
|
|
@ -145,108 +145,130 @@ struct ChannelForm: View {
|
|||
.toggleStyle(SwitchToggleStyle(tint: .accentColor))
|
||||
.disabled(!supportedVersion)
|
||||
.listRowSeparator(.visible)
|
||||
.onChange(of: preciseLocation) { _, pl in
|
||||
if pl == false {
|
||||
positionPrecision = 15
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if !preciseLocation {
|
||||
VStack(alignment: .leading) {
|
||||
Label("Approximate Location", systemImage: "location.slash.circle.fill")
|
||||
|
||||
Slider(value: $positionPrecision, in: 12...15, step: 1) {
|
||||
Slider(
|
||||
value: Binding(
|
||||
get: { Double(channel.positionPrecision) },
|
||||
set: { channel.positionPrecision = Int32($0) }
|
||||
),
|
||||
in: 12...15,
|
||||
step: 1
|
||||
) {
|
||||
} minimumValueLabel: {
|
||||
Image(systemName: "plus")
|
||||
} maximumValueLabel: {
|
||||
Image(systemName: "minus")
|
||||
}
|
||||
Text(PositionPrecision(rawValue: Int(positionPrecision))?.description ?? "")
|
||||
Text(PositionPrecision(rawValue: Int(channel.positionPrecision))?.description ?? "")
|
||||
.foregroundColor(.gray)
|
||||
.font(.callout)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Section(header: Text("MQTT")) {
|
||||
Toggle(isOn: $uplink) {
|
||||
Toggle(isOn: Binding(
|
||||
get: { channel.uplinkEnabled },
|
||||
set: { channel.uplinkEnabled = $0 }
|
||||
)) {
|
||||
Label("Uplink Enabled", systemImage: "arrowshape.up")
|
||||
}
|
||||
.toggleStyle(SwitchToggleStyle(tint: .accentColor))
|
||||
.listRowSeparator(.visible)
|
||||
|
||||
Toggle(isOn: $downlink) {
|
||||
Toggle(isOn: Binding(
|
||||
get: { channel.downlinkEnabled },
|
||||
set: { channel.downlinkEnabled = $0 }
|
||||
)) {
|
||||
Label("Downlink Enabled", systemImage: "arrowshape.down")
|
||||
}
|
||||
.toggleStyle(SwitchToggleStyle(tint: .accentColor))
|
||||
}
|
||||
}
|
||||
.onChange(of: channelName) {
|
||||
hasChanges = true
|
||||
}
|
||||
.onChange(of: channelKeySize) {
|
||||
if channelKeySize == -1 {
|
||||
channelKey = "AQ=="
|
||||
} else {
|
||||
let key = generateChannelKey(size: channelKeySize)
|
||||
channelKey = key
|
||||
}
|
||||
hasChanges = true
|
||||
}
|
||||
.onChange(of: channelKey) {
|
||||
hasChanges = true
|
||||
}
|
||||
.onChange(of: channelKeySize) {
|
||||
if channelKeySize == -1 {
|
||||
if channelRole == 0 {
|
||||
.onFirstAppear {
|
||||
supportedVersion = accessoryManager.checkIsVersionSupported(forVersion: minimumVersion)
|
||||
channelKey = channel.psk?.base64EncodedString() ?? ""
|
||||
channelKeySize = keySizeFromPsk(channel.psk)
|
||||
|
||||
if !supportedVersion {
|
||||
if channel.role == 1 {
|
||||
positionsEnabled = true
|
||||
if channelKey == "AQ==" {
|
||||
preciseLocation = false
|
||||
channel.positionPrecision = 14
|
||||
} else {
|
||||
preciseLocation = true
|
||||
channel.positionPrecision = 32
|
||||
}
|
||||
} else {
|
||||
positionsEnabled = false
|
||||
preciseLocation = false
|
||||
channel.positionPrecision = 0
|
||||
}
|
||||
} else {
|
||||
positionsEnabled = channel.positionPrecision > 0
|
||||
if channelKey == "AQ==" {
|
||||
preciseLocation = false
|
||||
let p = channel.positionPrecision
|
||||
if p > 0 && (p < 11 || p > 14) {
|
||||
channel.positionPrecision = 14
|
||||
}
|
||||
} else {
|
||||
preciseLocation = channel.positionPrecision == 32
|
||||
}
|
||||
channelKey = "AQ=="
|
||||
}
|
||||
}
|
||||
.onChange(of: channelRole) {
|
||||
hasChanges = true
|
||||
.onChange(of: channelKeySize) { _, size in
|
||||
if size == -1 {
|
||||
channelKey = "AQ=="
|
||||
} else if size > 0 {
|
||||
channelKey = generateChannelKey(size: size)
|
||||
} else {
|
||||
channelKey = ""
|
||||
}
|
||||
channel.psk = channelKey.isEmpty ? nil : Data(base64Encoded: channelKey)
|
||||
}
|
||||
.onChange(of: preciseLocation) { _, loc in
|
||||
if loc == true {
|
||||
.onChange(of: positionsEnabled) { _, enabled in
|
||||
if enabled {
|
||||
if channel.positionPrecision == 0 {
|
||||
channel.positionPrecision = 15
|
||||
}
|
||||
} else {
|
||||
channel.positionPrecision = 0
|
||||
preciseLocation = false
|
||||
}
|
||||
}
|
||||
.onChange(of: preciseLocation) { _, precise in
|
||||
if precise {
|
||||
if channelKey == "AQ==" || channelKeySize <= 1 {
|
||||
preciseLocation = false
|
||||
} else {
|
||||
positionPrecision = 32
|
||||
channel.positionPrecision = 32
|
||||
}
|
||||
} else {
|
||||
positionPrecision = 14
|
||||
}
|
||||
hasChanges = true
|
||||
}
|
||||
.onChange(of: positionPrecision) {
|
||||
hasChanges = true
|
||||
}
|
||||
.onChange(of: positionsEnabled) { _, pe in
|
||||
if pe {
|
||||
if positionPrecision == 0 {
|
||||
positionPrecision = 15
|
||||
if channel.positionPrecision == 32 {
|
||||
channel.positionPrecision = 14
|
||||
}
|
||||
} else {
|
||||
positionPrecision = 0
|
||||
}
|
||||
hasChanges = true
|
||||
}
|
||||
.onChange(of: uplink) {
|
||||
hasChanges = true
|
||||
}
|
||||
.onChange(of: downlink) {
|
||||
hasChanges = true
|
||||
}
|
||||
.onFirstAppear {
|
||||
let tempKey = Data(base64Encoded: channelKey) ?? Data()
|
||||
if tempKey.count == channelKeySize || channelKeySize == -1 {
|
||||
hasValidKey = true
|
||||
} else {
|
||||
hasValidKey = false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func keySizeFromPsk(_ psk: Data?) -> Int {
|
||||
let key = psk?.base64EncodedString() ?? ""
|
||||
if key.isEmpty { return 0 }
|
||||
if key == "AQ==" { return -1 }
|
||||
switch key.count {
|
||||
case 4: return 1
|
||||
case 24: return 16
|
||||
case 32: return 24
|
||||
case 44: return 32
|
||||
default: return 16
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -487,7 +487,9 @@ struct Settings: View {
|
|||
case .lora:
|
||||
LoRaConfig(node: nodes.first(where: { $0.num == selectedNode }))
|
||||
case .channels:
|
||||
Channels(node: node)
|
||||
if let node {
|
||||
Channels(node: node)
|
||||
}
|
||||
case .shareQRCode:
|
||||
ShareChannels(node: node)
|
||||
case .user:
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue