2022-07-14 07:14:43 -07:00
//
// S a v e C h a n n e l Q R C o d e . s w i f t
// M e s h t a s t i c
//
// C o p y r i g h t ( c ) G a r t h V a n d e r H o u w e n 7 / 1 3 / 2 2 .
//
import SwiftUI
2025-06-27 16:52:51 -07:00
import CoreData
import OSLog
import MeshtasticProtobufs
2022-07-14 07:14:43 -07:00
struct SaveChannelQRCode : View {
2022-11-08 23:18:50 -08:00
@ Environment ( \ . dismiss ) private var dismiss
2025-06-27 16:52:51 -07:00
@ Environment ( \ . managedObjectContext ) var context
2023-03-06 10:33:18 -08:00
2025-06-27 16:52:51 -07:00
let channelSetLink : String
2024-04-03 00:04:46 -07:00
var addChannels : Bool = false
2022-10-18 19:50:42 -07:00
var bleManager : BLEManager
2025-06-27 16:52:51 -07:00
@ State private var showError : Bool = false
@ State private var errorMessage : String = " "
@ State private var connectedToDevice : Bool = false
@ State private var loraChanges : [ String ] = [ ]
@ State private var okToMQTT : Bool = false
2022-10-18 19:50:42 -07:00
2022-07-14 07:14:43 -07:00
var body : some View {
VStack {
2024-04-03 00:04:46 -07:00
Text ( " \( addChannels ? " Add " : " Replace all " ) Channels? " )
2022-10-12 22:15:15 -07:00
. font ( . title )
2024-07-07 09:40:06 -07:00
Text ( " These settings will \( addChannels ? " add " : " replace all " ) channels. The current LoRa Config will be replaced, if there are substantial changes to the LoRa config the device will reboot " )
2024-05-05 08:38:27 -07:00
. fixedSize ( horizontal : false , vertical : true )
2022-10-12 22:15:15 -07:00
. foregroundColor ( . gray )
2024-04-03 00:04:46 -07:00
. font ( . title3 )
2022-10-12 22:15:15 -07:00
. padding ( )
2023-03-06 10:33:18 -08:00
2025-06-27 16:52:51 -07:00
if ! loraChanges . isEmpty {
VStack ( alignment : . leading ) {
Text ( " LoRa Config Changes: " )
. font ( . headline )
. padding ( . bottom , 5 )
ForEach ( loraChanges , id : \ . self ) { change in
Text ( " • \( change ) " )
. font ( . callout )
. foregroundColor ( . orange )
}
}
. padding ( )
}
2024-05-05 08:38:27 -07:00
if showError {
2025-06-27 16:52:51 -07:00
Text ( errorMessage . isEmpty ? " Channels being added from the QR code did not save. When adding channels the names must be unique. " : errorMessage )
2024-05-05 08:38:27 -07:00
. fixedSize ( horizontal : false , vertical : true )
. foregroundColor ( . red )
. font ( . callout )
. padding ( )
}
2025-06-27 16:52:51 -07:00
2022-11-08 22:01:32 -08:00
HStack {
2024-05-05 08:38:27 -07:00
if ! showError {
Button {
2025-06-27 16:52:51 -07:00
// E x t r a c t c h a n n e l d a t a i f i t ' s a f u l l U R L
let channelData : String
if channelSetLink . hasPrefix ( " http " ) || channelSetLink . hasPrefix ( " meshtastic:// " ) {
guard let extractedData = extractChannelDataFromURL ( channelSetLink ) else {
Logger . data . error ( " Failed to extract channel data from URL during save: \( channelSetLink ) " )
errorMessage = " Invalid channel URL format "
showError = true
return
}
channelData = extractedData
} else {
channelData = channelSetLink
}
let success = bleManager . saveChannelSet ( base64UrlString : channelData , addChannels : addChannels , okToMQTT : okToMQTT )
2024-05-05 08:38:27 -07:00
if success {
dismiss ( )
} else {
2025-06-27 16:52:51 -07:00
errorMessage = " Failed to save channel configuration "
2024-05-05 08:38:27 -07:00
showError = true
}
} label : {
2025-05-05 09:35:30 -07:00
Label ( " Save " , systemImage : " square.and.arrow.down " )
2022-10-19 16:58:49 -07:00
}
2024-05-05 08:38:27 -07:00
. buttonStyle ( . bordered )
. buttonBorderShape ( . capsule )
. controlSize ( . large )
. padding ( )
. disabled ( ! connectedToDevice )
2024-08-18 17:15:55 -07:00
2025-06-27 16:52:51 -07:00
#if targetEnvironment ( macCatalyst )
Button {
dismiss ( )
} label : {
Label ( " Cancel " , systemImage : " xmark " )
}
. buttonStyle ( . bordered )
. buttonBorderShape ( . capsule )
. controlSize ( . large )
. padding ( )
#endif
2024-05-05 08:38:27 -07:00
} else {
Button {
dismiss ( )
} label : {
2025-02-15 12:17:22 -08:00
Label ( " Cancel " , systemImage : " xmark " )
2024-05-05 08:38:27 -07:00
}
. buttonStyle ( . bordered )
. buttonBorderShape ( . capsule )
. controlSize ( . large )
. padding ( )
2022-10-10 21:21:58 -07:00
}
2022-11-08 22:01:32 -08:00
}
2022-10-18 19:50:42 -07:00
}
. onAppear {
2025-06-27 16:52:51 -07:00
Logger . data . info ( " Ch set link \( channelSetLink ) " )
2022-10-18 19:50:42 -07:00
connectedToDevice = bleManager . connectToPreferredPeripheral ( )
2025-06-27 16:52:51 -07:00
fetchLoRaConfigChanges ( )
}
}
private func extractChannelDataFromURL ( _ urlString : String ) -> String ? {
Logger . data . info ( " Extracting channel data from URL: \( urlString ) " )
if let url = URL ( string : urlString ) {
// G e t t h e f r a g m e n t ( p a r t a f t e r # )
if let fragment = url . fragment , ! fragment . isEmpty {
Logger . data . info ( " Extracted fragment from URL: \( fragment ) " )
return fragment
}
}
// F a l l b a c k : m a n u a l l y e x t r a c t e v e r y t h i n g a f t e r t h e l a s t #
if let hashIndex = urlString . lastIndex ( of : " # " ) {
let startIndex = urlString . index ( after : hashIndex )
let channelData = String ( urlString [ startIndex . . . ] )
if ! channelData . isEmpty {
Logger . data . info ( " Extracted channel data manually: \( channelData ) " )
return channelData
}
}
Logger . data . error ( " Failed to extract channel data from URL: \( urlString ) " )
return nil
}
private func fetchLoRaConfigChanges ( ) {
var currentLoRaConfig : Config . LoRaConfig ?
// F i r s t , e x t r a c t t h e a c t u a l c h a n n e l d a t a f r o m t h e U R L i f i t ' s a f u l l U R L
let channelData : String
if channelSetLink . hasPrefix ( " http " ) || channelSetLink . hasPrefix ( " meshtastic:// " ) {
guard let extractedData = extractChannelDataFromURL ( channelSetLink ) else {
Logger . data . error ( " Failed to extract channel data from URL: \( channelSetLink ) " )
errorMessage = " Invalid channel URL format "
showError = true
return
}
channelData = extractedData
} else {
// A s s u m e i t ' s a l r e a d y t h e b a s e 6 4 d a t a
channelData = channelSetLink
2022-10-10 21:21:58 -07:00
}
2025-06-27 16:52:51 -07:00
Logger . data . info ( " Processing channel data: \( channelData ) " )
// F e t c h c u r r e n t L o R a c o n f i g f r o m C o r e D a t a
let fetchRequest = NodeInfoEntity . fetchRequest ( )
fetchRequest . predicate = NSPredicate ( format : " num == %lld " , Int64 ( bleManager . connectedPeripheral ? . num ? ? 0 ) )
do {
let nodes = try context . fetch ( fetchRequest )
if let node = nodes . first {
currentLoRaConfig = node . loRaConfig ? . toProto ( )
}
} catch {
Logger . data . error ( " Failed to fetch NodeInfoEntity: \( error . localizedDescription , privacy : . public ) " )
}
// D e c o d e b a s e 6 4 u r l s t r i n g
let decodedString = channelData . base64urlToBase64 ( )
guard let decodedData = Data ( base64Encoded : decodedString ) else {
Logger . data . error ( " Invalid base64 for ChannelSet data: \( channelData , privacy : . public ) " )
errorMessage = " Invalid channel data format "
showError = true
return
}
do {
let channelSet = try ChannelSet ( serializedBytes : decodedData )
let newLoRaConfig = channelSet . loraConfig
var changes : [ String ] = [ ]
// P r e s e r v e u s e r ' s c u r r e n t o k T o M Q T T s e t t i n g
okToMQTT = currentLoRaConfig ? . configOkToMqtt ? ? false
if let current = currentLoRaConfig {
// C o m p a r e e a c h f i e l d a n d t r a c k c h a n g e s
if current . hopLimit != newLoRaConfig . hopLimit {
changes . append ( " Hop Limit: \( current . hopLimit ) -> \( newLoRaConfig . hopLimit ) " )
}
if current . region != newLoRaConfig . region {
let currentRegionDesc = RegionCodes ( rawValue : Int ( current . region . rawValue ) ) ? . description ? ? " Unknown "
let newRegionDesc = RegionCodes ( rawValue : Int ( newLoRaConfig . region . rawValue ) ) ? . description ? ? " Unknown "
changes . append ( " Region: \( currentRegionDesc ) -> \( newRegionDesc ) " )
}
if current . modemPreset != newLoRaConfig . modemPreset {
let currentPresetDesc = ModemPresets ( rawValue : Int ( current . modemPreset . rawValue ) ) ? . description ? ? " Unknown "
let newPresetDesc = ModemPresets ( rawValue : Int ( newLoRaConfig . modemPreset . rawValue ) ) ? . description ? ? " Unknown "
changes . append ( " Modem Preset: \( currentPresetDesc ) -> \( newPresetDesc ) " )
}
if current . usePreset != newLoRaConfig . usePreset {
changes . append ( " Use Preset: \( current . usePreset ) -> \( newLoRaConfig . usePreset ) " )
}
if current . txEnabled != newLoRaConfig . txEnabled {
changes . append ( " Transmit Enabled: \( current . txEnabled ) -> \( newLoRaConfig . txEnabled ) " )
}
if current . txPower != newLoRaConfig . txPower {
changes . append ( " Transmit Power: \( current . txPower ) dBm -> \( newLoRaConfig . txPower ) dBm " )
}
if current . channelNum != newLoRaConfig . channelNum {
changes . append ( " Channel Number: \( current . channelNum ) -> \( newLoRaConfig . channelNum ) " )
}
if current . bandwidth != newLoRaConfig . bandwidth {
changes . append ( " Bandwidth: \( current . bandwidth ) -> \( newLoRaConfig . bandwidth ) " )
}
if current . codingRate != newLoRaConfig . codingRate {
changes . append ( " Coding Rate: \( current . codingRate ) -> \( newLoRaConfig . codingRate ) " )
}
if current . spreadFactor != newLoRaConfig . spreadFactor {
changes . append ( " Spread Factor: \( current . spreadFactor ) -> \( newLoRaConfig . spreadFactor ) " )
}
if current . sx126XRxBoostedGain != newLoRaConfig . sx126XRxBoostedGain {
changes . append ( " RX Boosted Gain: \( current . sx126XRxBoostedGain ) -> \( newLoRaConfig . sx126XRxBoostedGain ) " )
}
if current . overrideFrequency != newLoRaConfig . overrideFrequency {
changes . append ( " Override Frequency: \( current . overrideFrequency ) -> \( newLoRaConfig . overrideFrequency ) " )
}
if current . ignoreMqtt != newLoRaConfig . ignoreMqtt {
changes . append ( " Ignore MQTT: \( current . ignoreMqtt ) -> \( newLoRaConfig . ignoreMqtt ) " )
}
} else {
// C o m p a r e a g a i n s t d e f a u l t v a l u e s w h e n n o c u r r e n t c o n f i g e x i s t s
let defaultConfig = getDefaultLoRaConfig ( )
if newLoRaConfig . hopLimit != defaultConfig . hopLimit {
changes . append ( " Hop Limit: \( defaultConfig . hopLimit ) -> \( newLoRaConfig . hopLimit ) " )
}
if newLoRaConfig . region != defaultConfig . region {
let newRegionDesc = RegionCodes ( rawValue : Int ( newLoRaConfig . region . rawValue ) ) ? . description ? ? " Unknown "
changes . append ( " Region: Unset -> \( newRegionDesc ) " )
}
if newLoRaConfig . modemPreset != defaultConfig . modemPreset {
let newPresetDesc = ModemPresets ( rawValue : Int ( newLoRaConfig . modemPreset . rawValue ) ) ? . description ? ? " Unknown "
changes . append ( " Modem Preset: Long Fast -> \( newPresetDesc ) " )
}
if newLoRaConfig . usePreset != defaultConfig . usePreset {
changes . append ( " Use Preset: \( defaultConfig . usePreset ) -> \( newLoRaConfig . usePreset ) " )
}
if newLoRaConfig . txEnabled != defaultConfig . txEnabled {
changes . append ( " Transmit Enabled: \( defaultConfig . txEnabled ) -> \( newLoRaConfig . txEnabled ) " )
}
if newLoRaConfig . txPower != defaultConfig . txPower {
changes . append ( " Transmit Power: \( defaultConfig . txPower ) dBm -> \( newLoRaConfig . txPower ) dBm " )
}
if newLoRaConfig . channelNum != defaultConfig . channelNum {
changes . append ( " Channel Number: \( defaultConfig . channelNum ) -> \( newLoRaConfig . channelNum ) " )
}
if newLoRaConfig . bandwidth != defaultConfig . bandwidth {
changes . append ( " Bandwidth: \( defaultConfig . bandwidth ) -> \( newLoRaConfig . bandwidth ) " )
}
if newLoRaConfig . codingRate != defaultConfig . codingRate {
changes . append ( " Coding Rate: \( defaultConfig . codingRate ) -> \( newLoRaConfig . codingRate ) " )
}
if newLoRaConfig . spreadFactor != defaultConfig . spreadFactor {
changes . append ( " Spread Factor: \( defaultConfig . spreadFactor ) -> \( newLoRaConfig . spreadFactor ) " )
}
if newLoRaConfig . sx126XRxBoostedGain != defaultConfig . sx126XRxBoostedGain {
changes . append ( " RX Boosted Gain: \( defaultConfig . sx126XRxBoostedGain ) -> \( newLoRaConfig . sx126XRxBoostedGain ) " )
}
if newLoRaConfig . overrideFrequency != defaultConfig . overrideFrequency {
changes . append ( " Override Frequency: \( defaultConfig . overrideFrequency ) -> \( newLoRaConfig . overrideFrequency ) " )
}
if newLoRaConfig . ignoreMqtt != defaultConfig . ignoreMqtt {
changes . append ( " Ignore MQTT: \( defaultConfig . ignoreMqtt ) -> \( newLoRaConfig . ignoreMqtt ) " )
}
}
loraChanges = changes
} catch {
Logger . data . error ( " Failed to decode ChannelSet: \( error . localizedDescription , privacy : . public ) " )
errorMessage = " Failed to decode channel configuration "
showError = true
}
}
private func getDefaultLoRaConfig ( ) -> Config . LoRaConfig {
var config = Config . LoRaConfig ( )
config . hopLimit = 3
config . region = . unset
config . modemPreset = . longFast
config . usePreset = true
config . txEnabled = true
config . txPower = 0
config . channelNum = 0
config . bandwidth = 0
config . codingRate = 0
config . spreadFactor = 0
config . sx126XRxBoostedGain = false
config . overrideFrequency = 0.0
config . ignoreMqtt = false
config . configOkToMqtt = false
return config
}
}
extension LoRaConfigEntity {
func toProto ( ) -> Config . LoRaConfig {
var config = Config . LoRaConfig ( )
config . hopLimit = UInt32 ( self . hopLimit )
config . region = Config . LoRaConfig . RegionCode ( rawValue : Int ( self . regionCode ) ) ? ? . unset
config . modemPreset = Config . LoRaConfig . ModemPreset ( rawValue : Int ( self . modemPreset ) ) ? ? . longFast
config . usePreset = self . usePreset
config . txEnabled = self . txEnabled
config . txPower = Int32 ( self . txPower )
config . channelNum = UInt32 ( self . channelNum )
config . bandwidth = UInt32 ( self . bandwidth )
config . codingRate = UInt32 ( self . codingRate )
config . spreadFactor = UInt32 ( self . spreadFactor )
config . sx126XRxBoostedGain = self . sx126xRxBoostedGain
config . overrideFrequency = self . overrideFrequency
config . ignoreMqtt = self . ignoreMqtt
config . configOkToMqtt = self . okToMqtt
return config
2022-07-14 07:14:43 -07:00
}
}