Meshtastic-Apple/Meshtastic/Views/Settings/Config/Module/MQTTConfig.swift

421 lines
15 KiB
Swift
Raw Normal View History

//
// MQTT.swift
// Meshtastic
//
// Copyright (c) Garth Vander Houwen 9/4/22.
//
import CoreLocation
import MeshtasticProtobufs
2024-06-03 02:17:55 -07:00
import OSLog
import SwiftUI
struct MQTTConfig: View {
2023-03-06 10:33:18 -08:00
@Environment(\.managedObjectContext) var context
@EnvironmentObject var bleManager: BLEManager
@Environment(\.dismiss) private var goBack
var node: NodeInfoEntity?
@State private var isPresentingSaveConfirm: Bool = false
@State var hasChanges: Bool = false
@State var enabled = false
2023-08-06 17:41:46 -07:00
@State var proxyToClientEnabled = false
@State var address = ""
@State var username = ""
@State var password = ""
@State var encryptionEnabled = true
@State var jsonEnabled = false
@State var tlsEnabled = false
2023-05-04 21:49:35 -07:00
@State var root = "msh"
@State var selectedTopic = ""
@State var mqttConnected: Bool = false
@State var defaultTopic = "msh/US"
@State var nearbyTopics = [String]()
2024-03-26 13:26:23 -07:00
@State var mapReportingEnabled = false
@State var mapPublishIntervalSecs = 3600
2024-09-08 11:10:58 -07:00
@State var mapPositionPrecision: Double = 14.0
let locale = Locale.current
2023-03-06 10:33:18 -08:00
var body: some View {
VStack {
Form {
if node != nil && node?.loRaConfig != nil {
let rc = RegionCodes(rawValue: Int(node?.loRaConfig?.regionCode ?? 0))
if rc?.dutyCycle ?? 0 > 0 && rc?.dutyCycle ?? 0 < 100 {
2024-02-14 21:43:29 -08:00
Text("Your region has a \(rc?.dutyCycle ?? 0)% duty cycle. MQTT is not advised when you are duty cycle restricted, the extra traffic will quickly overwhelm your LoRa mesh.")
.font(.callout)
.foregroundColor(.red)
}
}
ConfigHeader(title: "MQTT", config: \.mqttConfig, node: node, onAppear: setMqttValues)
Section(header: Text("options")) {
Toggle(isOn: $enabled) {
2024-03-26 07:54:16 -07:00
Label("enabled", systemImage: "dot.radiowaves.up.forward")
}
.toggleStyle(SwitchToggleStyle(tint: .accentColor))
Toggle(isOn: $proxyToClientEnabled) {
Label("mqtt.clientproxy", systemImage: "iphone.radiowaves.left.and.right")
Text("Utilizes the network connection on your phone to connect to MQTT.")
}
.toggleStyle(SwitchToggleStyle(tint: .accentColor))
2024-03-26 13:26:23 -07:00
if enabled && proxyToClientEnabled && node!.mqttConfig!.proxyToClientEnabled == true {
Toggle(isOn: $mqttConnected) {
Label(mqttConnected ? "mqtt.disconnect".localized : "mqtt.connect".localized, systemImage: "server.rack")
2024-04-05 17:14:04 -07:00
if bleManager.mqttError.count > 0 {
Text(bleManager.mqttError)
.fixedSize(horizontal: false, vertical: true)
.foregroundColor(.red)
}
}
.toggleStyle(SwitchToggleStyle(tint: .accentColor))
}
Toggle(isOn: $encryptionEnabled) {
Label("Encryption Enabled", systemImage: "lock.icloud")
}
.toggleStyle(SwitchToggleStyle(tint: .accentColor))
Toggle(isOn: $jsonEnabled) {
Label("JSON Enabled", systemImage: "ellipsis.curlybraces")
Text("JSON mode is a limited, unencrypted MQTT output for locally integrating with home assistant")
}
.toggleStyle(SwitchToggleStyle(tint: .accentColor))
2024-03-26 13:26:23 -07:00
}
2024-03-26 13:26:23 -07:00
Section(header: Text("Map Report")) {
2024-03-26 13:26:23 -07:00
Toggle(isOn: $mapReportingEnabled) {
Label("enabled", systemImage: "map")
}
.toggleStyle(SwitchToggleStyle(tint: .accentColor))
2024-03-26 13:26:23 -07:00
if mapReportingEnabled {
Picker("Map Publish Interval", selection: $mapPublishIntervalSecs ) {
ForEach(UpdateIntervals.allCases) { ui in
if ui.rawValue >= 3600 {
Text(ui.description)
}
}
}
.pickerStyle(DefaultPickerStyle())
VStack(alignment: .leading) {
Label("Approximate Location", systemImage: "location.slash.circle.fill")
Slider(value: $mapPositionPrecision, in: 11...14, step: 1) {
} minimumValueLabel: {
Image(systemName: "minus")
} maximumValueLabel: {
Image(systemName: "plus")
2024-03-26 13:26:23 -07:00
}
Text(PositionPrecision(rawValue: Int(mapPositionPrecision))?.description ?? "")
.foregroundColor(.gray)
.font(.callout)
2024-03-26 13:26:23 -07:00
}
}
}
Section(header: Text("Root Topic")) {
HStack {
Label("Root Topic", systemImage: "tree")
TextField("Root Topic", text: $root)
.foregroundColor(.gray)
2024-10-05 15:50:57 -07:00
.onChange(of: root) {
2024-09-22 08:03:18 -07:00
var totalBytes = root.utf8.count
2024-03-26 13:26:23 -07:00
// Only mess with the value if it is too big
2024-09-22 08:03:18 -07:00
while totalBytes > 30 {
root = String(root.dropLast())
2024-09-22 08:03:18 -07:00
totalBytes = root.utf8.count
2024-03-26 13:26:23 -07:00
}
2024-10-05 15:50:57 -07:00
}
2024-03-26 13:26:23 -07:00
.foregroundColor(.gray)
}
.keyboardType(.asciiCapable)
.scrollDismissesKeyboard(.interactively)
.disableAutocorrection(true)
.listRowSeparator(.hidden)
Text("The root topic to use for MQTT.")
.foregroundColor(.gray)
.font(.callout)
2024-03-26 13:26:23 -07:00
if nearbyTopics.count > 0 {
Picker("Nearby Topics", selection: $selectedTopic ) {
ForEach(nearbyTopics, id: \.self) { nt in
Text(nt)
}
}
.pickerStyle(InlinePickerStyle())
.listRowSeparator(.hidden)
Text("If the default region topic is too busy you can choose a more local topic.")
.foregroundColor(.gray)
.font(.callout)
}
2022-12-30 11:08:59 -08:00
}
Section(header: Text("Server")) {
HStack {
Label("Address", systemImage: "server.rack")
TextField("Server Address", text: $address)
.foregroundColor(.gray)
.autocapitalization(.none)
.disableAutocorrection(true)
2024-10-05 15:50:57 -07:00
.onChange(of: address) {
2024-09-22 08:03:18 -07:00
var totalBytes = address.utf8.count
// Only mess with the value if it is too big
2024-09-22 08:03:18 -07:00
while totalBytes > 62 {
address = String(address.dropLast())
2024-09-22 08:03:18 -07:00
totalBytes = address.utf8.count
2022-12-30 11:08:59 -08:00
}
hasChanges = true
2024-10-05 15:50:57 -07:00
}
.keyboardType(.default)
}
.autocorrectionDisabled()
HStack {
Label("mqtt.username", systemImage: "person.text.rectangle")
TextField("mqtt.username", text: $username)
.foregroundColor(.gray)
.autocapitalization(.none)
.disableAutocorrection(true)
2024-10-05 15:50:57 -07:00
.onChange(of: username) {
2024-09-22 08:03:18 -07:00
var totalBytes = username.utf8.count
// Only mess with the value if it is too big
2024-09-22 08:03:18 -07:00
while totalBytes > 62 {
username = String(username.dropLast())
2024-09-22 08:03:18 -07:00
totalBytes = username.utf8.count
2022-12-30 11:08:59 -08:00
}
hasChanges = true
2024-10-05 15:50:57 -07:00
}
.foregroundColor(.gray)
}
.keyboardType(.default)
.scrollDismissesKeyboard(.interactively)
HStack {
Label("password", systemImage: "wallet.pass")
TextField("password", text: $password)
.foregroundColor(.gray)
.autocapitalization(.none)
.disableAutocorrection(true)
2024-10-05 15:50:57 -07:00
.onChange(of: password) {
2024-09-22 08:03:18 -07:00
var totalBytes = password.utf8.count
// Only mess with the value if it is too big
2024-09-22 08:03:18 -07:00
while totalBytes > 62 {
password = String(password.dropLast())
2024-09-22 08:03:18 -07:00
totalBytes = password.utf8.count
}
hasChanges = true
2024-10-05 15:50:57 -07:00
}
.foregroundColor(.gray)
}
.keyboardType(.default)
.scrollDismissesKeyboard(.interactively)
.listRowSeparator(/*@START_MENU_TOKEN@*/.visible/*@END_MENU_TOKEN@*/)
2024-03-26 13:26:23 -07:00
Toggle(isOn: $tlsEnabled) {
Label("TLS Enabled", systemImage: "checkmark.shield.fill")
Text("Your MQTT Server must support TLS. Not available via the public mqtt server.")
}
2024-03-26 13:26:23 -07:00
.toggleStyle(SwitchToggleStyle(tint: .accentColor))
2023-05-04 21:49:35 -07:00
}
2024-03-26 14:57:00 -07:00
Text("For all Mqtt functionality other than the map report you must also set uplink and downlink for each channel you want to bridge over Mqtt.")
.font(.callout)
}
.scrollDismissesKeyboard(.interactively)
.disabled(self.bleManager.connectedPeripheral == nil || node?.mqttConfig == nil)
2022-12-30 11:08:59 -08:00
}
SaveConfigButton(node: node, hasChanges: $hasChanges) {
2023-02-22 20:31:08 -08:00
let connectedNode = getNodeInfo(id: bleManager.connectedPeripheral?.num ?? -1, context: context)
if connectedNode != nil {
var mqtt = ModuleConfig.MQTTConfig()
mqtt.enabled = self.enabled
mqtt.proxyToClientEnabled = self.proxyToClientEnabled
mqtt.address = self.address
mqtt.username = self.username
mqtt.password = self.password
mqtt.root = self.root
mqtt.encryptionEnabled = self.encryptionEnabled
mqtt.jsonEnabled = self.jsonEnabled
mqtt.tlsEnabled = self.tlsEnabled
2024-03-26 13:26:23 -07:00
mqtt.mapReportingEnabled = self.mapReportingEnabled
mqtt.mapReportSettings.positionPrecision = UInt32(self.mapPositionPrecision)
mqtt.mapReportSettings.publishIntervalSecs = UInt32(self.mapPublishIntervalSecs)
let adminMessageId = bleManager.saveMQTTConfig(config: mqtt, fromUser: connectedNode!.user!, toUser: node!.user!, adminIndex: connectedNode?.myInfo?.adminIndex ?? 0)
if adminMessageId > 0 {
// Should show a saved successfully alert once I know that to be true
// for now just disable the button after a successful save
hasChanges = false
goBack()
}
}
}
2022-12-13 07:49:46 -08:00
.navigationTitle("mqtt.config")
.navigationBarItems(
trailing: ZStack {
ConnectedDevice(
bluetoothOn: bleManager.isSwitchedOn,
deviceConnected: bleManager.connectedPeripheral != nil,
name: bleManager.connectedPeripheral?.shortName ?? "?"
)
}
)
2024-08-11 17:31:27 -07:00
.onChange(of: enabled) {
if $0 != node?.mqttConfig?.enabled { hasChanges = true }
}
.onChange(of: proxyToClientEnabled) { newProxyToClientEnabled in
if newProxyToClientEnabled {
jsonEnabled = false
}
if newProxyToClientEnabled != node?.mqttConfig?.proxyToClientEnabled { hasChanges = true }
}
2023-05-04 21:49:35 -07:00
.onChange(of: address) { newAddress in
if node != nil && node?.mqttConfig != nil {
if newAddress != node!.mqttConfig!.address { hasChanges = true }
}
}
.onChange(of: username) { newUsername in
if node != nil && node?.mqttConfig != nil {
if newUsername != node!.mqttConfig!.username { hasChanges = true }
}
}
.onChange(of: password) { newPassword in
if node != nil && node?.mqttConfig != nil {
if newPassword != node!.mqttConfig!.password { hasChanges = true }
}
}
.onChange(of: root) { newRoot in
if node != nil && node?.mqttConfig != nil {
if newRoot != node!.mqttConfig!.root { hasChanges = true }
}
}
.onChange(of: selectedTopic) { newSelectedTopic in
root = newSelectedTopic
}
2024-08-11 17:31:27 -07:00
.onChange(of: encryptionEnabled) {
if $0 != node?.mqttConfig?.encryptionEnabled { hasChanges = true }
}
.onChange(of: jsonEnabled) { newJsonEnabled in
if newJsonEnabled {
proxyToClientEnabled = false
}
2024-08-11 17:31:27 -07:00
if newJsonEnabled != node?.mqttConfig?.jsonEnabled { hasChanges = true }
}
.onChange(of: tlsEnabled) { newTlsEnabled in
if address.lowercased() == "mqtt.meshtastic.org" {
tlsEnabled = false
} else {
if newTlsEnabled != node?.mqttConfig?.tlsEnabled { hasChanges = true }
}
2023-05-04 21:49:35 -07:00
}
.onChange(of: mqttConnected) { newMqttConnected in
if newMqttConnected == false {
if bleManager.mqttProxyConnected {
bleManager.mqttManager.disconnect()
}
} else {
if !bleManager.mqttProxyConnected && node != nil {
bleManager.mqttManager.connectFromConfigSettings(node: node!)
}
}
}
2024-08-11 17:31:27 -07:00
.onChange(of: mapReportingEnabled) {
if $0 != node?.mqttConfig?.mapReportingEnabled { hasChanges = true }
2024-03-26 13:26:23 -07:00
}
.onChange(of: mapPublishIntervalSecs) { newMapPublishIntervalSecs in
if node != nil && node?.mqttConfig != nil {
if newMapPublishIntervalSecs != node!.mqttConfig!.mapPublishIntervalSecs { hasChanges = true }
}
}
.onFirstAppear {
// Need to request a MqttModuleConfig from the remote node before allowing changes
if let connectedPeripheral = bleManager.connectedPeripheral, let node {
2024-06-03 02:17:55 -07:00
Logger.mesh.info("empty mqtt module config")
let connectedNode = getNodeInfo(id: connectedPeripheral.num, context: context)
if let connectedNode {
if node.num != connectedNode.num {
if UserDefaults.enableAdministration && node.num != connectedNode.num {
/// 2.5 Administration with session passkey
let expiration = node.sessionExpiration ?? Date()
if expiration < Date() || node.mqttConfig == nil {
_ = bleManager.requestMqttModuleConfig(fromUser: connectedNode.user!, toUser: node.user!, adminIndex: connectedNode.myInfo?.adminIndex ?? 0)
}
} else {
/// Legacy Administration
_ = bleManager.requestMqttModuleConfig(fromUser: connectedNode.user!, toUser: node.user!, adminIndex: connectedNode.myInfo?.adminIndex ?? 0)
}
}
2024-03-26 13:26:23 -07:00
}
}
}
}
func setMqttValues() {
2024-10-05 10:44:46 -07:00
nearbyTopics = []
let geocoder = CLGeocoder()
if LocationsHandler.shared.locationsArray.count > 0 {
let region = RegionCodes(rawValue: Int(node?.loRaConfig?.regionCode ?? 0))?.topic
defaultTopic = "msh/" + (region ?? "UNSET")
geocoder.reverseGeocodeLocation(LocationsHandler.shared.locationsArray.first!, completionHandler: {(placemarks, error) in
if let error {
Logger.services.error("Failed to reverse geocode location: \(error.localizedDescription)")
return
}
2024-10-05 10:44:46 -07:00
if let placemarks = placemarks, let placemark = placemarks.first {
let cc = locale.region?.identifier ?? "UNK"
/// Country Topic unless you are US
if placemark.isoCountryCode ?? "unknown" != cc {
let countryTopic = defaultTopic + "/" + (placemark.isoCountryCode ?? "")
if !countryTopic.isEmpty {
nearbyTopics.append(countryTopic)
}
}
2024-10-05 10:44:46 -07:00
let stateTopic = defaultTopic + "/" + (placemark.administrativeArea ?? "")
if !stateTopic.isEmpty {
nearbyTopics.append(stateTopic)
}
let countyTopic = defaultTopic + "/" + (placemark.administrativeArea ?? "") + "/" + (placemark.subAdministrativeArea?.lowercased().replacingOccurrences(of: " ", with: "") ?? "")
if !countyTopic.isEmpty {
nearbyTopics.append(countyTopic)
}
let cityTopic = defaultTopic + "/" + (placemark.administrativeArea ?? "") + "/" + (placemark.locality?.lowercased().replacingOccurrences(of: " ", with: "") ?? "")
if !cityTopic.isEmpty {
nearbyTopics.append(cityTopic)
}
let neightborhoodTopic = defaultTopic + "/" + (placemark.administrativeArea ?? "") + "/" + (placemark.subLocality?.lowercased()
.replacingOccurrences(of: " ", with: "")
.replacingOccurrences(of: "'", with: "") ?? "")
if !neightborhoodTopic.isEmpty {
nearbyTopics.append(neightborhoodTopic)
}
} else {
Logger.services.debug("No Location")
}
})
}
2024-10-05 10:44:46 -07:00
2024-03-26 13:26:23 -07:00
self.enabled = node?.mqttConfig?.enabled ?? false
self.proxyToClientEnabled = node?.mqttConfig?.proxyToClientEnabled ?? false
self.address = node?.mqttConfig?.address ?? ""
self.username = node?.mqttConfig?.username ?? ""
self.password = node?.mqttConfig?.password ?? ""
2023-05-04 21:49:35 -07:00
self.root = node?.mqttConfig?.root ?? "msh"
2024-03-26 13:26:23 -07:00
self.encryptionEnabled = node?.mqttConfig?.encryptionEnabled ?? false
self.jsonEnabled = node?.mqttConfig?.jsonEnabled ?? false
self.tlsEnabled = node?.mqttConfig?.tlsEnabled ?? false
self.mqttConnected = bleManager.mqttProxyConnected
2024-03-26 13:26:23 -07:00
self.mapReportingEnabled = node?.mqttConfig?.mapReportingEnabled ?? false
self.mapPublishIntervalSecs = Int(node?.mqttConfig?.mapPublishIntervalSecs ?? 3600)
self.mapPositionPrecision = Double(node?.mqttConfig?.mapPositionPrecision ?? 14)
if mapPositionPrecision < 11 || mapPositionPrecision > 14 {
self.mapPositionPrecision = 14
self.hasChanges = true
} else {
self.hasChanges = false
2024-03-26 13:26:23 -07:00
}
}
}