mirror of
https://github.com/meshtastic/Meshtastic-Apple.git
synced 2026-04-20 22:13:56 +00:00
417 lines
15 KiB
Swift
417 lines
15 KiB
Swift
//
|
|
// MQTT.swift
|
|
// Meshtastic
|
|
//
|
|
// Copyright (c) Garth Vander Houwen 9/4/22.
|
|
//
|
|
import CoreLocation
|
|
import MeshtasticProtobufs
|
|
import OSLog
|
|
import SwiftUI
|
|
|
|
struct MQTTConfig: View {
|
|
|
|
@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
|
|
@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
|
|
@State var root = "msh"
|
|
@State var selectedTopic = ""
|
|
@State var mqttConnected: Bool = false
|
|
@State var defaultTopic = "msh/US"
|
|
@State var nearbyTopics = [String]()
|
|
@State var mapReportingEnabled = false
|
|
@State var mapPublishIntervalSecs = 3600
|
|
@State var mapPositionPrecision: Double = 14.0
|
|
|
|
let locale = Locale.current
|
|
|
|
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 {
|
|
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) {
|
|
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))
|
|
|
|
if enabled && proxyToClientEnabled && node?.mqttConfig?.proxyToClientEnabled ?? false == true {
|
|
Toggle(isOn: $mqttConnected) {
|
|
Label(mqttConnected ? "mqtt.disconnect".localized : "mqtt.connect".localized, systemImage: "server.rack")
|
|
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))
|
|
|
|
if !proxyToClientEnabled {
|
|
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))
|
|
}
|
|
}
|
|
|
|
Section(header: Text("Map Report")) {
|
|
|
|
Toggle(isOn: $mapReportingEnabled) {
|
|
Label("enabled", systemImage: "map")
|
|
}
|
|
.toggleStyle(SwitchToggleStyle(tint: .accentColor))
|
|
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")
|
|
}
|
|
Text(PositionPrecision(rawValue: Int(mapPositionPrecision))?.description ?? "")
|
|
.foregroundColor(.gray)
|
|
.font(.callout)
|
|
}
|
|
}
|
|
}
|
|
Section(header: Text("Root Topic")) {
|
|
HStack {
|
|
Label("Root Topic", systemImage: "tree")
|
|
TextField("Root Topic", text: $root)
|
|
.foregroundColor(.gray)
|
|
.onChange(of: root) {
|
|
var totalBytes = root.utf8.count
|
|
// Only mess with the value if it is too big
|
|
while totalBytes > 30 {
|
|
root = String(root.dropLast())
|
|
totalBytes = root.utf8.count
|
|
}
|
|
}
|
|
.foregroundColor(.gray)
|
|
}
|
|
.keyboardType(.asciiCapable)
|
|
.scrollDismissesKeyboard(.interactively)
|
|
.disableAutocorrection(true)
|
|
.listRowSeparator(.hidden)
|
|
Text("The root topic to use for MQTT.")
|
|
.foregroundColor(.gray)
|
|
.font(.callout)
|
|
|
|
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)
|
|
}
|
|
}
|
|
|
|
Section(header: Text("Server")) {
|
|
HStack {
|
|
Label("Address", systemImage: "server.rack")
|
|
TextField("Server Address", text: $address)
|
|
.foregroundColor(.gray)
|
|
.autocapitalization(.none)
|
|
.disableAutocorrection(true)
|
|
.onChange(of: address) {
|
|
var totalBytes = address.utf8.count
|
|
// Only mess with the value if it is too big
|
|
while totalBytes > 62 {
|
|
address = String(address.dropLast())
|
|
totalBytes = address.utf8.count
|
|
}
|
|
hasChanges = true
|
|
}
|
|
.keyboardType(.default)
|
|
}
|
|
.autocorrectionDisabled()
|
|
if address != "mqtt.meshtastic.org" {
|
|
HStack {
|
|
Label("mqtt.username", systemImage: "person.text.rectangle")
|
|
TextField("mqtt.username", text: $username)
|
|
.foregroundColor(.gray)
|
|
.autocapitalization(.none)
|
|
.disableAutocorrection(true)
|
|
.onChange(of: username) {
|
|
var totalBytes = username.utf8.count
|
|
// Only mess with the value if it is too big
|
|
while totalBytes > 62 {
|
|
username = String(username.dropLast())
|
|
totalBytes = username.utf8.count
|
|
}
|
|
hasChanges = true
|
|
}
|
|
.foregroundColor(.gray)
|
|
}
|
|
.keyboardType(.default)
|
|
.scrollDismissesKeyboard(.interactively)
|
|
HStack {
|
|
Label("password", systemImage: "wallet.pass")
|
|
TextField("password", text: $password)
|
|
.foregroundColor(.gray)
|
|
.autocapitalization(.none)
|
|
.disableAutocorrection(true)
|
|
.onChange(of: password) {
|
|
var totalBytes = password.utf8.count
|
|
// Only mess with the value if it is too big
|
|
while totalBytes > 30 {
|
|
password = String(password.dropLast())
|
|
totalBytes = password.utf8.count
|
|
}
|
|
hasChanges = true
|
|
}
|
|
.foregroundColor(.gray)
|
|
}
|
|
.keyboardType(.default)
|
|
.scrollDismissesKeyboard(.interactively)
|
|
.listRowSeparator(/*@START_MENU_TOKEN@*/.visible/*@END_MENU_TOKEN@*/)
|
|
if !proxyToClientEnabled {
|
|
Toggle(isOn: $tlsEnabled) {
|
|
Label("TLS Enabled", systemImage: "checkmark.shield.fill")
|
|
Text("Your MQTT Server must support TLS.")
|
|
}
|
|
.toggleStyle(SwitchToggleStyle(tint: .accentColor))
|
|
}
|
|
}
|
|
}
|
|
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)
|
|
|
|
SaveConfigButton(node: node, hasChanges: $hasChanges) {
|
|
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
|
|
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()
|
|
}
|
|
}
|
|
}.onChange(of: enabled) { _, newEnabled in
|
|
if newEnabled != node?.mqttConfig?.enabled { hasChanges = true }
|
|
}
|
|
.onChange(of: proxyToClientEnabled) { _, newProxyToClientEnabled in
|
|
if newProxyToClientEnabled {
|
|
jsonEnabled = false
|
|
tlsEnabled = false
|
|
}
|
|
if newProxyToClientEnabled != node?.mqttConfig?.proxyToClientEnabled { hasChanges = true }
|
|
}
|
|
.onChange(of: address) { _, newAddress in
|
|
if newAddress != node?.mqttConfig?.address ?? "" { hasChanges = true }
|
|
}
|
|
.onChange(of: username) { _, newUsername in
|
|
if newUsername != node?.mqttConfig?.username ?? "" { hasChanges = true }
|
|
}
|
|
.onChange(of: password) { _, newPassword in
|
|
if newPassword != node?.mqttConfig?.password ?? "" { hasChanges = true }
|
|
}
|
|
.onChange(of: root) { _, newRoot in
|
|
if newRoot != node?.mqttConfig?.root ?? "" { hasChanges = true }
|
|
}
|
|
.onChange(of: selectedTopic) { _, newSelectedTopic in
|
|
root = newSelectedTopic
|
|
}
|
|
.onChange(of: encryptionEnabled) { _, newEncryptionEnabled in
|
|
if newEncryptionEnabled != node?.mqttConfig?.encryptionEnabled { hasChanges = true }
|
|
}
|
|
.onChange(of: jsonEnabled) { _, newJsonEnabled in
|
|
if newJsonEnabled {
|
|
proxyToClientEnabled = false
|
|
}
|
|
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 }
|
|
}
|
|
}
|
|
.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!)
|
|
}
|
|
}
|
|
}
|
|
.onChange(of: mapReportingEnabled) { _, newMapReportingEnabled in
|
|
if newMapReportingEnabled != node?.mqttConfig?.mapReportingEnabled { hasChanges = true }
|
|
}
|
|
.onChange(of: mapPublishIntervalSecs) { _, newMapPublishIntervalSecs in
|
|
if newMapPublishIntervalSecs != node?.mqttConfig?.mapPublishIntervalSecs ?? -1 { hasChanges = true }
|
|
}
|
|
}
|
|
.navigationTitle("mqtt.config")
|
|
.navigationBarItems(
|
|
trailing: ZStack {
|
|
ConnectedDevice(
|
|
bluetoothOn: bleManager.isSwitchedOn,
|
|
deviceConnected: bleManager.connectedPeripheral != nil,
|
|
name: bleManager.connectedPeripheral?.shortName ?? "?"
|
|
)
|
|
}
|
|
)
|
|
.onFirstAppear {
|
|
// Need to request a MqttModuleConfig from the remote node before allowing changes
|
|
if let connectedPeripheral = bleManager.connectedPeripheral, let node {
|
|
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 {
|
|
Logger.mesh.info("⚙️ Empty or expired mqtt module config requesting via PKI admin")
|
|
_ = bleManager.requestMqttModuleConfig(fromUser: connectedNode.user!, toUser: node.user!, adminIndex: connectedNode.myInfo?.adminIndex ?? 0)
|
|
}
|
|
} else {
|
|
/// Legacy Administration
|
|
Logger.mesh.info("☠️ Using insecure legacy admin, empty mqtt module config")
|
|
_ = bleManager.requestMqttModuleConfig(fromUser: connectedNode.user!, toUser: node.user!, adminIndex: connectedNode.myInfo?.adminIndex ?? 0)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
func setMqttValues() {
|
|
|
|
nearbyTopics = []
|
|
let geocoder = CLGeocoder()
|
|
if LocationsHandler.shared.locationsArray.count > 0 {
|
|
let region = RegionCodes(rawValue: Int(node?.loRaConfig?.regionCode ?? 0))
|
|
defaultTopic = "msh/" + (region?.topic ?? "UNSET")
|
|
geocoder.reverseGeocodeLocation(LocationsHandler.shared.locationsArray.first!, completionHandler: {(placemarks, error) in
|
|
if let error {
|
|
Logger.services.error("Failed to reverse geocode location: \(error.localizedDescription, privacy: .public)")
|
|
return
|
|
}
|
|
|
|
if let placemarks = placemarks, let placemark = placemarks.first {
|
|
let cc = locale.region?.identifier ?? "UNK"
|
|
/// Country Topic unless your region is a country
|
|
if !(region?.isCountry ?? false) {
|
|
let countryTopic = defaultTopic + "/" + (placemark.isoCountryCode ?? "")
|
|
if !countryTopic.isEmpty {
|
|
nearbyTopics.append(countryTopic)
|
|
}
|
|
}
|
|
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")
|
|
}
|
|
})
|
|
}
|
|
|
|
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 ?? ""
|
|
self.root = node?.mqttConfig?.root ?? "msh"
|
|
self.encryptionEnabled = node?.mqttConfig?.encryptionEnabled ?? false
|
|
self.jsonEnabled = node?.mqttConfig?.jsonEnabled ?? false
|
|
self.tlsEnabled = node?.mqttConfig?.tlsEnabled ?? false
|
|
self.mqttConnected = bleManager.mqttProxyConnected
|
|
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
|
|
}
|
|
}
|
|
}
|