mirror of
https://github.com/meshtastic/Meshtastic-Apple.git
synced 2026-04-20 22:13:56 +00:00
687 lines
20 KiB
Swift
687 lines
20 KiB
Swift
//
|
|
// TAKServerManager.swift
|
|
// Meshtastic
|
|
//
|
|
// Created by niccellular 12/26/25
|
|
//
|
|
|
|
import Foundation
|
|
import Network
|
|
import OSLog
|
|
import Combine
|
|
import SwiftUI
|
|
import SwiftData
|
|
import MeshtasticProtobufs
|
|
|
|
enum TAKServerError: LocalizedError {
|
|
case noServerCertificate
|
|
case noClientCACertificate
|
|
case tlsConfigurationFailed
|
|
case listenerFailed(String)
|
|
case clientNotFound
|
|
case notRunning
|
|
case primaryChannelInvalid(String)
|
|
|
|
var errorDescription: String? {
|
|
switch self {
|
|
case .noServerCertificate:
|
|
return "No server certificate configured. Import a .p12 file with the server certificate and private key."
|
|
case .noClientCACertificate:
|
|
return "No client CA certificate configured. Import the CA certificate (.pem) used to sign client certificates."
|
|
case .tlsConfigurationFailed:
|
|
return "Failed to configure TLS settings."
|
|
case .listenerFailed(let reason):
|
|
return "Failed to start listener: \(reason)"
|
|
case .clientNotFound:
|
|
return "Client not found"
|
|
case .notRunning:
|
|
return "TAK Server is not running"
|
|
case .primaryChannelInvalid(let reason):
|
|
return reason
|
|
}
|
|
}
|
|
}
|
|
|
|
struct PrimaryChannelIssue: Identifiable {
|
|
let id = UUID()
|
|
let title: String
|
|
let description: String
|
|
let canAutoFix: Bool
|
|
}
|
|
|
|
/// Manages the TAK Server lifecycle, TLS connections, and client management
|
|
/// Runs on MainActor for thread safety, following the AccessoryManager pattern
|
|
@MainActor
|
|
final class TAKServerManager: ObservableObject {
|
|
|
|
static let shared = TAKServerManager()
|
|
|
|
// MARK: - Published State
|
|
|
|
@Published private(set) var isRunning = false
|
|
@Published private(set) var connectedClients: [TAKClientInfo] = []
|
|
@Published var lastError: String?
|
|
@Published private(set) var primaryChannelIssues: [PrimaryChannelIssue] = []
|
|
@Published private(set) var readOnlyMode = false
|
|
|
|
/// User toggle for read-only mode - locked to true if channel has issues
|
|
@AppStorage("takServerReadOnly") var userReadOnlyMode = false
|
|
|
|
/// Enable Mesh to CoT converter - bridges Meshtastic packets to TAK format
|
|
@AppStorage("takServerMeshToCot") var meshToCotEnabled = false
|
|
|
|
// MARK: - Configuration (persisted via AppStorage)
|
|
|
|
@AppStorage("takServerChannel") var channel: Int = 0
|
|
|
|
@AppStorage("takServerEnabled") var enabled = false {
|
|
didSet {
|
|
Task {
|
|
if enabled && !isRunning {
|
|
try? await start()
|
|
} else if !enabled && isRunning {
|
|
stop()
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Fixed port - always use TLS port 8089
|
|
static let defaultTLSPort = 8089
|
|
static let defaultTCPPort = 8087 // Legacy, not used
|
|
|
|
/// Port is fixed to 8089 (mTLS)
|
|
var port: Int { Self.defaultTLSPort }
|
|
|
|
/// Always use TLS/mTLS
|
|
var useTLS: Bool { true }
|
|
|
|
// MARK: - Bridge
|
|
|
|
/// Bridge for converting between CoT and Meshtastic formats
|
|
var bridge: TAKMeshtasticBridge?
|
|
|
|
// MARK: - Private Properties
|
|
|
|
private var listener: NWListener?
|
|
private var connections: [ObjectIdentifier: TAKConnection] = [:]
|
|
private var connectionTasks: [ObjectIdentifier: Task<Void, Never>] = [:]
|
|
private let queue = DispatchQueue(label: "tak.server", qos: .userInitiated)
|
|
|
|
private init() {}
|
|
|
|
// MARK: - Initialization
|
|
|
|
/// Initialize the TAK server on app startup
|
|
/// Call this from app initialization to restore server state
|
|
func initializeOnStartup() {
|
|
guard enabled else {
|
|
Logger.tak.debug("TAK Server not enabled, skipping startup")
|
|
return
|
|
}
|
|
|
|
guard !isRunning else {
|
|
Logger.tak.debug("TAK Server already running")
|
|
return
|
|
}
|
|
|
|
Logger.tak.info("TAK Server enabled, starting on app launch")
|
|
Task {
|
|
do {
|
|
try await start()
|
|
} catch {
|
|
Logger.tak.error("Failed to start TAK Server on startup: \(error.localizedDescription)")
|
|
}
|
|
}
|
|
}
|
|
|
|
// MARK: - Primary Channel Validation
|
|
|
|
/// Check the primary channel for validity
|
|
/// Returns true if the primary channel is valid for TAK server operation
|
|
func checkPrimaryChannelValidity() {
|
|
let context = PersistenceController.shared.context
|
|
let descriptor = FetchDescriptor<MyInfoEntity>()
|
|
|
|
var issues: [PrimaryChannelIssue] = []
|
|
var isValid = true
|
|
|
|
do {
|
|
let myInfos = try context.fetch(descriptor)
|
|
guard let myInfo = myInfos.first,
|
|
let primaryChannel = myInfo.channels.first(where: { $0.index == 0 || $0.role == 1 }) else {
|
|
issues.append(PrimaryChannelIssue(
|
|
title: "No Primary Channel",
|
|
description: "No primary channel found on device",
|
|
canAutoFix: false
|
|
))
|
|
isValid = false
|
|
updateChannelStatus(issues: issues, isValid: isValid)
|
|
return
|
|
}
|
|
|
|
let channelName = primaryChannel.name ?? ""
|
|
let channelPsk = primaryChannel.psk ?? Data()
|
|
let pskBase64 = channelPsk.base64EncodedString()
|
|
|
|
if channelName.isEmpty {
|
|
issues.append(PrimaryChannelIssue(
|
|
title: "Unnamed Primary Channel",
|
|
description: "TAK Server requires a private channel. Please set up a dedicated TAK channel (name 'TAK' recommended). Tap the button below to auto-configure.",
|
|
canAutoFix: true
|
|
))
|
|
isValid = false
|
|
}
|
|
|
|
// Use byte length for encryption strength checks (not Base64 string length)
|
|
let pskBytes = channelPsk.count
|
|
if pskBytes == 0 {
|
|
issues.append(PrimaryChannelIssue(
|
|
title: "Public Channel Not Supported",
|
|
description: "TAK Server requires a private channel with encryption. Public channels expose your location and messages. Tap the button below to set up a private TAK channel.",
|
|
canAutoFix: true
|
|
))
|
|
isValid = false
|
|
} else if channelPsk == Data([0x01]) {
|
|
// Default key is single byte 0x01
|
|
issues.append(PrimaryChannelIssue(
|
|
title: "Default Encryption Key",
|
|
description: "TAK Server requires a unique private channel key. The default key is not secure. Tap the button below to set up a proper private TAK channel.",
|
|
canAutoFix: true
|
|
))
|
|
isValid = false
|
|
} else if pskBytes < 16 {
|
|
// Less than 128-bit (16 bytes)
|
|
issues.append(PrimaryChannelIssue(
|
|
title: "Weak Encryption Key",
|
|
description: "TAK Server requires at least 128-bit encryption for your privacy. Tap the button below to set up a secure private TAK channel.",
|
|
canAutoFix: true
|
|
))
|
|
isValid = false
|
|
}
|
|
|
|
updateChannelStatus(issues: issues, isValid: isValid)
|
|
|
|
} catch {
|
|
Logger.tak.error("Failed to fetch MyInfo for channel validation: \(error.localizedDescription)")
|
|
issues.append(PrimaryChannelIssue(
|
|
title: "Error Checking Channel",
|
|
description: "Could not verify primary channel settings",
|
|
canAutoFix: false
|
|
))
|
|
updateChannelStatus(issues: issues, isValid: false)
|
|
}
|
|
}
|
|
|
|
private func updateChannelStatus(issues: [PrimaryChannelIssue], isValid: Bool) {
|
|
primaryChannelIssues = issues
|
|
readOnlyMode = !isValid
|
|
|
|
if !isValid {
|
|
userReadOnlyMode = true
|
|
}
|
|
|
|
if !isValid && isRunning {
|
|
Logger.tak.warning("TAK Server running in read-only mode due to primary channel issues")
|
|
}
|
|
}
|
|
|
|
/// Check if TAK client messages should be forwarded to mesh
|
|
var shouldForwardTAKToMesh: Bool {
|
|
return !userReadOnlyMode
|
|
}
|
|
|
|
// MARK: - Server Lifecycle
|
|
|
|
/// Start the TAK server (TLS or TCP based on configuration)
|
|
func start() async throws {
|
|
guard !isRunning else {
|
|
Logger.tak.info("TAK Server already running")
|
|
return
|
|
}
|
|
|
|
checkPrimaryChannelValidity()
|
|
|
|
let mode = useTLS ? "TLS" : "TCP"
|
|
Logger.tak.info("Starting TAK Server on port \(self.port) (\(mode) mode)")
|
|
|
|
let parameters: NWParameters
|
|
|
|
if useTLS {
|
|
// Validate we have a server certificate for TLS mode
|
|
guard let identity = TAKCertificateManager.shared.getServerIdentity() else {
|
|
let error = TAKServerError.noServerCertificate
|
|
lastError = error.localizedDescription
|
|
enabled = false
|
|
throw error
|
|
}
|
|
|
|
// Create TLS options
|
|
let tlsOptions = NWProtocolTLS.Options()
|
|
|
|
// Set server identity (certificate + private key)
|
|
guard let secIdentity = sec_identity_create(identity) else {
|
|
let error = TAKServerError.tlsConfigurationFailed
|
|
Logger.tak.error("Failed to create sec_identity from server identity")
|
|
lastError = error.localizedDescription
|
|
enabled = false
|
|
throw error
|
|
}
|
|
sec_protocol_options_set_local_identity(
|
|
tlsOptions.securityProtocolOptions,
|
|
secIdentity
|
|
)
|
|
|
|
// Set minimum TLS version to 1.2 (TAK standard)
|
|
sec_protocol_options_set_min_tls_protocol_version(
|
|
tlsOptions.securityProtocolOptions,
|
|
.TLSv12
|
|
)
|
|
|
|
// Configure mTLS - always require client certificate for TLS mode
|
|
sec_protocol_options_set_peer_authentication_required(
|
|
tlsOptions.securityProtocolOptions,
|
|
true
|
|
)
|
|
|
|
// Set up client certificate validation
|
|
let clientCAs = TAKCertificateManager.shared.getClientCACertificates()
|
|
Logger.tak.info("Loaded \(clientCAs.count) CA certificate(s) for client validation")
|
|
if !clientCAs.isEmpty {
|
|
for (index, ca) in clientCAs.enumerated() {
|
|
if let summary = SecCertificateCopySubjectSummary(ca) as String? {
|
|
Logger.tak.info("CA[\(index)]: \(summary)")
|
|
}
|
|
}
|
|
let trustRoots = clientCAs as CFArray
|
|
sec_protocol_options_set_verify_block(
|
|
tlsOptions.securityProtocolOptions,
|
|
{ _, secTrust, completion in
|
|
// Convert sec_trust_t to SecTrust
|
|
let trust = sec_trust_copy_ref(secTrust).takeRetainedValue()
|
|
|
|
// Set policy for client certificate validation
|
|
// Use SSL policy with server=false to validate client certificates
|
|
// This properly accepts clientAuth ExtendedKeyUsage
|
|
let clientPolicy = SecPolicyCreateSSL(false, nil)
|
|
SecTrustSetPolicies(trust, clientPolicy)
|
|
|
|
SecTrustSetAnchorCertificates(trust, trustRoots)
|
|
SecTrustSetAnchorCertificatesOnly(trust, true)
|
|
var error: CFError?
|
|
let isValid = SecTrustEvaluateWithError(trust, &error)
|
|
if let error = error {
|
|
Logger.tak.error("Client cert validation error: \(error.localizedDescription)")
|
|
}
|
|
Logger.tak.info("Client certificate validation: \(isValid ? "passed" : "failed")")
|
|
completion(isValid)
|
|
},
|
|
queue
|
|
)
|
|
} else {
|
|
// No client CAs configured: keep mTLS enabled but reject all client certificates
|
|
Logger.tak.warning("mTLS enabled but no CA certificates configured for client validation; all client connections will be rejected")
|
|
sec_protocol_options_set_verify_block(
|
|
tlsOptions.securityProtocolOptions,
|
|
{ _, _, completion in
|
|
Logger.tak.error("Rejecting client connection because no client CA certificates are configured")
|
|
completion(false)
|
|
},
|
|
queue
|
|
)
|
|
}
|
|
|
|
// TCP options
|
|
let tcpOptions = NWProtocolTCP.Options()
|
|
tcpOptions.enableKeepalive = true
|
|
tcpOptions.keepaliveIdle = 60
|
|
|
|
parameters = NWParameters(tls: tlsOptions, tcp: tcpOptions)
|
|
} else {
|
|
// Plain TCP mode (no TLS)
|
|
let tcpOptions = NWProtocolTCP.Options()
|
|
tcpOptions.enableKeepalive = true
|
|
tcpOptions.keepaliveIdle = 60
|
|
|
|
parameters = NWParameters(tls: nil, tcp: tcpOptions)
|
|
}
|
|
|
|
parameters.allowLocalEndpointReuse = true
|
|
|
|
// Bind to localhost only - only allow TAK clients on the same device
|
|
parameters.requiredLocalEndpoint = NWEndpoint.hostPort(
|
|
host: NWEndpoint.Host("127.0.0.1"),
|
|
port: NWEndpoint.Port(integerLiteral: UInt16(port))
|
|
)
|
|
|
|
// Create and configure listener
|
|
do {
|
|
listener = try NWListener(using: parameters)
|
|
} catch {
|
|
lastError = "Failed to create listener: \(error.localizedDescription)"
|
|
Logger.tak.error("Failed to create TAK listener: \(error.localizedDescription)")
|
|
enabled = false
|
|
throw error
|
|
}
|
|
|
|
// Set up state handler
|
|
listener?.stateUpdateHandler = { [weak self] state in
|
|
Task { @MainActor in
|
|
self?.handleListenerState(state)
|
|
}
|
|
}
|
|
|
|
// Set up new connection handler
|
|
listener?.newConnectionHandler = { [weak self] connection in
|
|
Task { @MainActor in
|
|
await self?.handleNewConnection(connection)
|
|
}
|
|
}
|
|
|
|
// Start listening
|
|
listener?.start(queue: queue)
|
|
}
|
|
|
|
/// Stop the TAK server
|
|
func stop() {
|
|
Logger.tak.info("Stopping TAK Server")
|
|
|
|
listener?.cancel()
|
|
listener = nil
|
|
|
|
// Cancel all connection tasks
|
|
for (_, task) in connectionTasks {
|
|
task.cancel()
|
|
}
|
|
connectionTasks.removeAll()
|
|
|
|
// Disconnect all clients
|
|
for (_, connection) in connections {
|
|
Task {
|
|
await connection.disconnect()
|
|
}
|
|
}
|
|
connections.removeAll()
|
|
connectedClients.removeAll()
|
|
|
|
isRunning = false
|
|
lastError = nil
|
|
|
|
Logger.tak.info("TAK Server stopped")
|
|
}
|
|
|
|
/// Restart the server (useful after configuration changes)
|
|
func restart() async throws {
|
|
stop()
|
|
try await Task.sleep(nanoseconds: 500_000_000) // 0.5s delay
|
|
try await start()
|
|
}
|
|
|
|
// MARK: - State Handling
|
|
|
|
private func handleListenerState(_ state: NWListener.State) {
|
|
switch state {
|
|
case .ready:
|
|
isRunning = true
|
|
lastError = nil
|
|
Logger.tak.info("TAK Server listening on port \(self.port)")
|
|
|
|
case .failed(let error):
|
|
isRunning = false
|
|
lastError = error.localizedDescription
|
|
enabled = false
|
|
Logger.tak.error("TAK Server failed: \(error.localizedDescription)")
|
|
|
|
case .cancelled:
|
|
isRunning = false
|
|
Logger.tak.info("TAK Server cancelled")
|
|
|
|
case .waiting(let error):
|
|
Logger.tak.warning("TAK Server waiting: \(error.localizedDescription)")
|
|
|
|
case .setup:
|
|
Logger.tak.debug("TAK Server setup")
|
|
|
|
@unknown default:
|
|
break
|
|
}
|
|
}
|
|
|
|
// MARK: - Connection Management
|
|
|
|
private func handleNewConnection(_ nwConnection: NWConnection) async {
|
|
let connectionId = ObjectIdentifier(nwConnection)
|
|
let connection = TAKConnection(connection: nwConnection)
|
|
|
|
connections[connectionId] = connection
|
|
|
|
Logger.tak.info("New TAK client connecting: \(nwConnection.endpoint.debugDescription)")
|
|
|
|
// Start handling the connection
|
|
let eventStream = await connection.start()
|
|
|
|
// Create task to handle connection events
|
|
let task = Task {
|
|
for await event in eventStream {
|
|
await handleConnectionEvent(event, connectionId: connectionId)
|
|
}
|
|
// Connection ended
|
|
await removeConnection(connectionId)
|
|
}
|
|
|
|
connectionTasks[connectionId] = task
|
|
}
|
|
|
|
private func handleConnectionEvent(_ event: TAKConnectionEvent, connectionId: ObjectIdentifier) async {
|
|
switch event {
|
|
case .connected(let clientInfo):
|
|
connectedClients.append(clientInfo)
|
|
Logger.tak.info("TAK client connected: \(clientInfo.displayName)")
|
|
|
|
// Send all mesh node positions to the newly connected client
|
|
if meshToCotEnabled {
|
|
await bridge?.broadcastAllNodesToTAK()
|
|
}
|
|
|
|
case .clientInfoUpdated(let clientInfo):
|
|
// Update the client info in our list
|
|
if let index = connectedClients.firstIndex(where: { $0.id == clientInfo.id }) {
|
|
connectedClients[index] = clientInfo
|
|
}
|
|
|
|
case .message(let cotMessage):
|
|
Logger.tak.info("Received CoT from TAK client: \(cotMessage.type)")
|
|
// Forward to Meshtastic mesh via bridge
|
|
await bridge?.sendToMesh(cotMessage)
|
|
|
|
case .disconnected:
|
|
await removeConnection(connectionId)
|
|
|
|
case .error(let error):
|
|
Logger.tak.error("TAK client error: \(error.localizedDescription)")
|
|
}
|
|
}
|
|
|
|
private func removeConnection(_ connectionId: ObjectIdentifier) async {
|
|
connectionTasks[connectionId]?.cancel()
|
|
connectionTasks.removeValue(forKey: connectionId)
|
|
|
|
if let connection = connections.removeValue(forKey: connectionId) {
|
|
let endpoint = await connection.endpoint
|
|
connectedClients.removeAll { $0.endpoint.debugDescription == endpoint.debugDescription }
|
|
Logger.tak.info("TAK client disconnected")
|
|
}
|
|
}
|
|
|
|
// MARK: - Message Distribution
|
|
|
|
/// Broadcast a CoT message to all connected TAK clients
|
|
func broadcast(_ cotMessage: CoTMessage) async {
|
|
guard !connections.isEmpty else { return }
|
|
|
|
Logger.tak.info("Broadcasting CoT to \(self.connections.count) TAK client(s): \(cotMessage.type)")
|
|
|
|
for (connectionId, connection) in connections {
|
|
do {
|
|
try await connection.send(cotMessage)
|
|
} catch {
|
|
Logger.tak.error("Failed to send to TAK client: \(error.localizedDescription)")
|
|
// Remove failed connection
|
|
await removeConnection(connectionId)
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Ensure bridge is initialized and ready for mesh-to-CoT broadcasting
|
|
/// Returns true if broadcasting is possible (meshToCotEnabled, server running, clients connected)
|
|
/// Call this before any mesh-to-CoT broadcast operations
|
|
func ensureBridgeReadyForMeshToCot() -> Bool {
|
|
guard meshToCotEnabled, isRunning, !connectedClients.isEmpty else { return false }
|
|
|
|
if bridge == nil {
|
|
Logger.tak.info("Initializing bridge for mesh-to-CoT broadcast")
|
|
let accessoryManager = AccessoryManager.shared
|
|
let newBridge = TAKMeshtasticBridge(
|
|
accessoryManager: accessoryManager,
|
|
takServerManager: self
|
|
)
|
|
newBridge.context = accessoryManager.context
|
|
bridge = newBridge
|
|
}
|
|
return true
|
|
}
|
|
|
|
/// Send a CoT message to a specific client
|
|
func send(_ cotMessage: CoTMessage, to clientId: UUID) async throws {
|
|
guard let clientInfo = connectedClients.first(where: { $0.id == clientId }) else {
|
|
throw TAKServerError.clientNotFound
|
|
}
|
|
|
|
for (_, connection) in connections {
|
|
let endpoint = await connection.endpoint
|
|
if endpoint.debugDescription == clientInfo.endpoint.debugDescription {
|
|
try await connection.send(cotMessage)
|
|
return
|
|
}
|
|
}
|
|
|
|
throw TAKServerError.clientNotFound
|
|
}
|
|
|
|
// MARK: - Auto-fix Primary Channel
|
|
|
|
/// Automatically fix the primary channel to TAK-compatible settings
|
|
/// Sets: Name="TAK", 256-bit AES key, preserves existing LoRa channel
|
|
/// Returns true if successful
|
|
func autoFixPrimaryChannel() async -> Bool {
|
|
let accessoryManager = AccessoryManager.shared
|
|
|
|
guard accessoryManager.isConnected else {
|
|
Logger.tak.error("Cannot fix channel: Not connected to device")
|
|
return false
|
|
}
|
|
|
|
Logger.tak.info("Auto-fixing primary channel for TAK compatibility")
|
|
|
|
let context = PersistenceController.shared.context
|
|
|
|
guard let connectedNodeNum = accessoryManager.activeDeviceNum else {
|
|
Logger.tak.error("Cannot fix channel: No active device number")
|
|
return false
|
|
}
|
|
|
|
guard let connectedNode = getNodeInfo(id: connectedNodeNum, context: context),
|
|
let user = connectedNode.user else {
|
|
Logger.tak.error("Cannot fix channel: No connected node or user found")
|
|
return false
|
|
}
|
|
|
|
let descriptor = FetchDescriptor<MyInfoEntity>()
|
|
|
|
do {
|
|
let myInfos = try context.fetch(descriptor)
|
|
guard let myInfo = myInfos.first,
|
|
let primaryChannel = myInfo.channels.first(where: { $0.index == 0 || $0.role == 1 }) else {
|
|
Logger.tak.error("Cannot fix channel: No primary channel found")
|
|
return false
|
|
}
|
|
|
|
let newKey = generateChannelKey(size: 32)
|
|
guard let newPsk = Data(base64Encoded: newKey) else {
|
|
Logger.tak.error("Failed to decode generated channel key; aborting primary channel fix")
|
|
return false
|
|
}
|
|
|
|
primaryChannel.name = "TAK"
|
|
primaryChannel.psk = newPsk
|
|
primaryChannel.role = 1
|
|
primaryChannel.index = 0
|
|
|
|
if let idx = myInfo.channels.firstIndex(of: primaryChannel), idx != 0 {
|
|
myInfo.channels.remove(at: idx)
|
|
myInfo.channels.insert(primaryChannel, at: 0)
|
|
}
|
|
|
|
try context.save()
|
|
|
|
var channel = Channel()
|
|
channel.index = 0
|
|
channel.role = .primary
|
|
channel.settings.name = "TAK"
|
|
channel.settings.psk = newPsk
|
|
channel.settings.uplinkEnabled = primaryChannel.uplinkEnabled
|
|
channel.settings.downlinkEnabled = primaryChannel.downlinkEnabled
|
|
channel.settings.moduleSettings.positionPrecision = UInt32(primaryChannel.positionPrecision)
|
|
|
|
try await accessoryManager.saveChannel(channel: channel, fromUser: user, toUser: user)
|
|
|
|
Logger.tak.info("Successfully fixed primary channel: name=TAK, key=256-bit")
|
|
|
|
// Also set LoRa modem preset to shortFast for optimal TAK performance
|
|
var loraConfig = Config.LoRaConfig()
|
|
loraConfig.modemPreset = .shortFast
|
|
loraConfig.usePreset = true
|
|
loraConfig.txEnabled = true
|
|
loraConfig.hopLimit = 3
|
|
|
|
// Get current LoRa config to preserve other settings
|
|
if let currentLoRa = connectedNode.loRaConfig {
|
|
loraConfig.region = Config.LoRaConfig.RegionCode(rawValue: Int(currentLoRa.regionCode)) ?? .unset
|
|
loraConfig.channelNum = UInt32(currentLoRa.channelNum)
|
|
loraConfig.txPower = Int32(currentLoRa.txPower)
|
|
loraConfig.bandwidth = UInt32(currentLoRa.bandwidth)
|
|
loraConfig.codingRate = UInt32(currentLoRa.codingRate)
|
|
loraConfig.spreadFactor = UInt32(currentLoRa.spreadFactor)
|
|
}
|
|
|
|
do {
|
|
try await accessoryManager.saveLoRaConfig(config: loraConfig, fromUser: user, toUser: user)
|
|
Logger.tak.info("Successfully set LoRa modem preset to shortFast")
|
|
} catch {
|
|
Logger.tak.warning("Failed to set LoRa modem preset: \(error.localizedDescription)")
|
|
}
|
|
|
|
checkPrimaryChannelValidity()
|
|
|
|
return true
|
|
|
|
} catch {
|
|
Logger.tak.error("Failed to fix primary channel: \(error.localizedDescription)")
|
|
return false
|
|
}
|
|
}
|
|
|
|
// MARK: - Status
|
|
|
|
/// Get server status description
|
|
var statusDescription: String {
|
|
if isRunning {
|
|
let mode = useTLS ? "TLS" : "TCP"
|
|
return "Running on port \(port) (\(mode))"
|
|
} else if let error = lastError {
|
|
return "Error: \(error)"
|
|
} else {
|
|
return "Stopped"
|
|
}
|
|
}
|
|
}
|